typescript - Kan jag skapa en mer specifik typ än string?


Till exempel har jag en användartyp:

type User = {
  name: string;
  city: string;
  email: Email;
  password: Password;
}

Jag kan använda string i e-post men jag har vissa begränsningar och därför vill jag skriva typen Email här. Begränsningarna är:

  1. Det måste ha någon form. Endast tecken a-z är tillåtna och 2 specialtecken, . och @. Exempel, [email är giltig form, [email inte giltig.

  2. Den måste vara av begränsad längd.

Något som:

type Email = /pattern here/

Är det möjligt att göra den här typen?

Svar #1

Det rätta svaret på detta är förmodligen en nominal-liknande typ som använder branding som i @LionelRowe:s svar; om du inte hanterar strängliteraltyper, så att dessa e-postadresssträngar är hårdkodade som strängliteraler i din kod, finns det inget sätt för kompilatorn att upprätthålla ett sådant krav. Kompilatorn kommer bara att se string.

Även om kompilatorn känner till de exakta strängliteralerna du anger, såsom const str="[email ", är det bortom TypeScript 4.0:s förmåga att validera strängar som dessa.


Det fantastiska är att det inte kommer att vara bortom förmågorna hos TypeScript 4.1, som kommer att introducera template literal-typer (som implementerats i microsoft/TypeScript#40336)... som faktiskt kan utföra operationer på strängliteraler.

Tyvärr, så vitt jag kan se, kommer det att vara i gränslandet för TypeScripts möjligheter. För att få kompilatorn att verifiera att en sträng inte är längre än (säga) 32 tecken och att den endast måste innehålla alfabetiska tecken, ".", och "@", måste du använda generics och antagligen recursiva villkorstyper (som även är planerade för TS4.1 enligt implementeringen i microsoft/TypeScript#40002). Vid varje steg kommer du att behöva undvika olika gränser för rekursion eller kombinatorik; det är lätt att skriva något som bryter kompilatorn för strängar på bara omkring 10 tecken. Även när du lyckas undvika gränserna tenderar kompilatorn att vara långsam. Skriv in en ogiltig e-postadress och vänta 10 sekunder innan kompilatorn visar ett fel. Det är ett kaos och jag rekommenderar det inte, åtminstone inte i TS4.1.

Men jag älskar det här för mycket för att inte göra det:

type Lower = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
type AllowedChars = Lower | '.' | '@'

type RestrictedToChars<T extends string, A extends string, Y = T, N = never> =
  string extends T ? N :
  T extends `${infer F}${infer F}${infer F}${infer F}${infer F}${infer F}${infer R}` ?
  [F] extends [A] ? RestrictedToChars<R, A, Y, N> : N :
  T extends `${infer F}${infer F}${infer F}${infer R}` ?
  [F] extends [A] ? RestrictedToChars<R, A, Y, N> : N :
  T extends `${infer F}${infer R}` ?
  [F] extends [A] ? RestrictedToChars<R, A, Y, N> : N :
  Y

Typen RestrictedToChars<T, A, Y, N> tar ett strängliteral T och en union av tillåtna tecken A, och utvärderas till: Y om T endast innehåller tecken i A, eller N om T innehåller några tecken inte i A.

type SplitChars<T extends string> = string extends T ? string[] :
  T extends `${infer F1}${infer F2}${infer F3}${infer F4}${infer F5}${infer F6}${infer R}` ? [F1, F2, F3, F4, F5, F6, ...SplitChars<R>] :
  T extends `${infer F1}${infer F2}${infer F3}${infer R}` ? [F1, F2, F3, ...SplitChars<R>] :
  T extends `${infer F1}${infer R}` ? [F1, ...SplitChars<R>] :
  []

type CheckMaxLength<T extends string, L extends number, Y = T, N = never> =
  SplitChars<T>[L] extends undefined ? Y : N;

Typen CheckMaxLength<T, L, Y, N> tar en strängliteral T och en maximal numerisk längd L, och utvärderas till: Y om T är högst L tecken lång, eller N om T är längre än L tecken.

Du kan sedan definiera Email<T> som en generell validator på en strängliteraltyp T, och göra User<T> generell på samma sätt:

type Email<T extends string> = T &
  CheckMaxLength<T, 32, RestrictedToChars<
    `${lowercase T}`, AllowedChars, T, ["Email can only contain alphabet or @ or ."]
  >, ["Email needs to be 32 characters or less"]>;

type User<T extends string> = {
  name: string;
  email: Email<T>;
}

Slutligen kan du se kompilatorn göra valideringen:

const user = <T extends string>(user: User<T>) => user;

user({ name: "Dave Morrison", email: "[email " }); // okay
user({ name: "Van Morrison", email: "[email " }); // error!
// ------------------------> ~~~~~
// '["Email can only contain alphabet or @ or ."]'
user({ name: "Jim Morrison", email: "[email " }); // error!
// ------------------------> ~~~~~
// '["Email needs to be 32 characters or less"]'

Utsökt fruktansvärt! Återigen, jag rekommenderar inte att du använder detta. Men när TS4.1 släpps kommer det att vara tekniskt möjligt att låta kompilatorn validera strängar på detta sätt.

Lekplatslänk

Svar #2

Den enda typen som är mer specifik än sträng skulle vara en specifik sträng:

type Email = '[email ' | '[email '

Det är allt du kan göra, tyvärr. Specifika Typescript-typer är inte avsedda för att specificera något om generisk användarinmatning, förutom dess typ, om inmatningen inte är begränsad till några få hårdkodade värden. Av liknande skäl kan du inte göra något som

type BigNum = number > 1000;

Relaterade artiklar