TypeScript-generiska fungerar när du anropar dem men inte i funktionskroppen?
Säg att jag har detta exempel:
type Keys = 'one' | 'two';
type Values<T extends Keys> = T extends 'one'
? { id: 1, name: 'hello1' }
: T extends 'two'
? { id: 2, value: 'hello2' }
: never;
type Fn = <T extends Keys>(key: T) => (value: Values<T>) => void;
const fn: Fn = (key) => (value) => {
if (key === 'one') {
const name = value.name; // This throws compile error
}
}
// Both of these `fn` calls correctly limit the `value` argument based on the `key`
fn('one')({ id: 1, name: 'hello1' });
fn('two')({ id: 2, value: 'hello2' });
Kan någon förklara för mig varför denna uppsättning korrekt begränsar de tillåtna typerna för argumentet value när man anropar fn-metoden, men inte låter mig specificera value mer efter att ha använt en Type Guard inuti fn-implementeringen?
Lekplats - Jag har denna lekplats med några andra försök att få detta att fungera, men utan framgång.
I slutändan försöker jag använda något liknande detta som en useCallback-metod i ett React-projekt där en onChange-prop sätts med callbacken som anropas med en nyckel, och sedan skickar eventet värdet. Eftersom jag känner till typerna av eventet känns det som att detta borde vara möjligt. Ex:
<Component1 onChange={handleChange('key1')} />
<Component2 onChange={handleChange('key2')} />
Svar #1
Först låt oss slopa din villkorliga typdefinition av Values<K>. Detta kan enkelt omstruktureras till en indexerad åtkomst i ett kartläggningsgränssnitt:
interface KeyVals {
one: { id: 1, name: "hello1" },
two: { id: 2, value: "hello2" }
}
type Keys = keyof KeyVals;
type Values<K extends Keys> = KeyVals[K];
Kompileraren är något bättre på att analysera indexerade åtkomster än den är på att analysera villkorliga typer. Detta löser inte ditt problem, men det gör att min lösning fungerar bättre, så jag kommer att göra det.
Huvudproblemet här är skillnaden mellan hur kompilatorn hanterar diskriminerade unioner och hur den hanterar generiska typer. Din fn()-funktionsanropssignatur är generisk, med värden k av typen K och v av typen Values<K>, men din funktionsimplementation behöver behandla dessa som diskriminerade unioner, där kontroll av värdet på k bör begränsa typen av v. Men att kontrollera k begränsar endast typen av k, och begränsar inte typ-parametern K. Så du kan inte behandla dessa generiska värden som diskriminerade unioner.
Detta är ett slags allmän begränsning i TypeScript för närvarande, med olika problem på GitHub som har tagits upp om olika aspekter av det. Till exempel finns microsoft/TypeScript#33014, som uttryckligen begär en kontroll på k för att begränsa typparametern K. Det problemet är för närvarande öppet. Det skulle antagligen också behöva microsoft/TypeScript#27808 implementerat, så att man kan förhindra att folk skickar in en union för K, eftersom det ställer till problem. Det finns också microsoft/TypeScript#30581 som frågar hur man hanterar unioner generiskt, och den rekommenderade lösningen/tillvägagångssättet för det på microsoft/TypeScript#47109 är i princip att omformulera unioner som generiska typer... men här behöver du att unionbeteendet fungerar. Just nu verkar detta helt enkelt inte vara möjligt.
Du försökte också tillvägagångssättet att göra fn() till en icke-generisk överlagrad funktion. Detta skulle helt undvika generics. Tyvärr är kompilatorn ännu mindre kapabel att analysera överlagringar än den är med generics. Inuti implementeringen av en överlagrad funktions statement begränsar inte kompilatorn saker och ting till att endast vara kompatibla med anropets signaturer; den tittar endast på implementeringssignaturen, och den matchar implementeringen och anropssignaturerna mycket löst. Se microsoft/TypeScript#13235 för en avslagen begäran om att ha överlagringar kontrollerade på ett typ-säkert sätt. Och om du använder ett överlagt funktions expression så är kompilatorn för strikt. Se microsoft/TypeScript#47669 för en fortfarande öppen begäran om att göra något annorlunda här. Så överlagringar kan antingen göras för att undertrycka fel (men tillåta vissa osäkra saker) eller fånga fel (men förhindra vissa säkra saker) men inte båda.
Så vitt jag vet finns det inget sätt att refaktorera din kod så att kompilatorn tillåter dig att göra rätt sak samtidigt som den hindrar dig från att göra fel sak. Allt jag kan tänka mig är en lösning.
I det här fallet är det närmaste jag kan komma till användbar säkerhet att använda en funktionsöverlagringssats för att överbrygga gapet mellan generiska typer och diskriminerade unioner. Det ser ut så här:
function uncurriedFn<K extends Keys>(k: K, v: Values<K>): void;
function uncurriedFn(...[k, v]: { [K in Keys]: [k: K, v: Values<K>] }[Keys]) {
if (k === 'one') {
const name = v.name;
}
}
Så uncurriedFn() är en enskild funktion som tar både k och v istället för den currade versionen som tar k och returnerar en funktion som tar v. Funktionssignaturen är generisk, så du bör kunna anropa den säkert som en generisk funktion. Faktum är att det låter dig använda den inuti din avsedda currade version:
type Fn = <K extends Keys>(k: K) => (v: Values<K>) => void;
const fn: Fn = (k) => (v) => uncurriedFn(k, v)
fn('one')({ id: 1, name: 'hello1' });
fn('two')({ id: 2, value: 'hello2' });
Så det är anropssignaturen. Vad sägs om implementeringen? Jo, implementeringssignaturen är icke-generisk och tar ett destrukturerat diskriminerat union-argument. Typen {[K in Keys]: [k: K, v: Values<K>]}[Keys] är ett distributivt objekt (som myntats i ms/TS#47109) som utvärderas till [k: 'one', v: Values<'one'>] | [k: 'two', v: Values<'two'>]. Detta tillåter också endast säkra anrop; det kommer att klaga om du matchar k och v felaktigt. Men nu kan implementeringskroppen fortsätta att använda k och v som ett destrukturerat diskriminerat union; kontrollera k, och v är begränsad.
Och där har du det. Allting är, faktiskt, säkert (eller så säkert som rimligt... generics kan användas osäkert ibland), och kompilatorn klagar inte. Tricket är, återigen, detta par av anrops- och implementeringssignaturer:
function uncurriedFn<K extends Keys>(k: K, v: Values<K>): void;
function uncurriedFn(...[k, v]: { [K in Keys]: [k: K, v: Values<K>] }[Keys]) {}
I en ideal värld skulle kompilatorn veta att signaturen <K extends Keys>(k: K, v: Values<K>) => void och (...args: {[K in Keys]: [k: K, v: Values<K>]}[Keys]) => void är kompatibla (med undantag för ms/TS#27808). Men den vet inte detta, så du måste lura den genom att utnyttja slappheten i kontrollen av överlagringssatser.
Lekplatslänk till kod
