generics - Hur skapar man en TypeScript-funktion som returnerar en sträng eller undefined baserat på att indata är en sträng eller undefined?


Det jag försöker skriva i ren Javascript skulle vara:

const cleanVar = (var1): T => var1?.replace(searchValue, replaceValue);

I Typescript skulle jag förvänta mig att kunna skriva detta som:

const cleanVar = <T extends string | undefined>(var1: T): T => var1?.replace(searchValue, replaceValue);

Men jag får det här felet:

Type 'string | undefined' is not assignable to type 'T'.
  'string | undefined' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'string | undefined'.
    Type 'undefined' is not assignable to type 'T'.
      'undefined' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'string | undefined'.

Så, vad är det korrekta sättet att skriva den här funktionen på?

Betydelsen bör vara: var kan vara antingen string eller undefined. Om var är string måste returtypen vara string. Om var är undefined måste returtypen vara undefined.

Svar #1

Det är inte sant att cleanVar() returnerar T, eftersom string-literaltyper finns, och om du därför anropar cleanVar("hello"), kommer den härledda typen av T att vara "hello" och inte string. Även om du kunde ordna anropet till replace() så att det faktiskt var en identitetsfunktion vid körning (t.ex. v.replace("a","a")), vet kompilatorn om returtypen för String.prototype.replace() enligt TS standardbiblioteket bara att det är en string. Eftersom string innehåller värden som inte kan tilldelas T, klagar kompilatorn korrekt.


Den "rätta" anropssignaturen för cleanVar() är förmodligen denna:

declare const cleanVar: <T extends string | undefined>(v: T) => 
  T extends string ? string : undefined;

Det vill säga, det bör returnera en villkorlig typ som distribueras över unioner i T. Utrustad med denna signatur, låt oss verifiera att det fungerar som du vill när det anropas:

const x = cleanVar("hello") // const x: string
const y = cleanVar(undefined) // const y: undefined;
const z = cleanVar(Math.random() < 0.5 ? "hello" : undefined) // const z: string | undefined

Ser bra ut!


Tyvärr kommer du fortfarande att få ett kompileringsfel i din implementation:

const cleanVar = <T extends string | undefined>(v: T): 
  T extends string ? string : undefined =>
  v?.replace(searchValue, replaceValue); // error!
// Type 'string | undefined' is not assignable to type 'T extends string ? string : undefined'.

Och det beror på att kompilatorn egentligen inte kan verifiera att något specifikt värde kan tilldelas en konditionell typ som beror på en oupplöst generisk typparameter, såsom T inom kroppen av cleanVars implementering. Den skjuter upp utvärderingen av sådana konditionella typer, som förblir "opaka". Detta är en känd svårighet i TypeScript, och det finns ett öppet problem på microsoft/TypeScript#33912 som frågar efter någon lösning där typparametern T kan begränsas baserat på kontrollflödesanalys. Just nu kommer v?.xyz att begränsa v från T till string inom xyz, men inget du gör i funktionen kan begränsa T självt från T extends string | undefined till T extends string. Och vi sitter fast.

För tillfället är det enda sättet att göra detta med en typassertion; där ansvaret för att verifiera korrektheten i din implementering flyttas från kompilatorn (som inte kan göra det) till dig... så var försiktig. Hur som helst ser det ut så här:

const cleanVar = <T extends string | undefined>(v: T) =>
  v?.replace(searchValue, replaceValue) as T extends string ? string : undefined;

Ett annat "rätt" sätt att gå vidare skulle vara att göra cleanVar() till en överlagrad funktion med flera anropssignaturer, så här:

function cleanVar(v: string): string;
function cleanVar(v: undefined): undefined;
function cleanVar(v: string | undefined): string | undefined;
function cleanVar(v: string | undefined) {
  return v?.replace(searchValue, replaceValue);
}

Detta fungerar på liknande sätt som den generiska villkorsversionen, men är mer omständligt. Du kan inte enkelt göra detta till en pilfunktion utan typpåståenden (vilket skulle motverka syftet), och även med funktionsdeklarationer som ovan är avsaknaden av fel i implementeringen liknande att använda ett typpåstående... kompilatorn bryr sig bara om att implementeringssignaturen matchas. Om du ändrade det till

function cleanVarBad(v: string): string;
function cleanVarBad(v: undefined): undefined;
function cleanVarBad(v: string | undefined): string | undefined;
function cleanVarBad(v: string | undefined): string | undefined {
  return Math.random() < 0.5 ? "oopsie" : undefined;
}

då skulle du fortfarande inte få ett fel, men du skulle förmodligen vara väldigt missnöjd vid körning. Så det finns egentligen ingen större fördel med den ena över den andra; jag skulle generellt föredra generiskt-betingade anropssignaturer framför överlagringar.

Lekplatslänk till kod

Relaterade artiklar