microsoft / microsoft/TypeScript
Union of user-defined type guards spawns incorrect type
Nessuno ha ancora preso questa issue.
- Lingua principale
- Go
- Stelle
- 111k
- Fork
- 14.3k
- Merge medio
- 1g 19h
- PR unite (30g)
- 117
Descrizione
🔎 Search Terms
- Type predicate
- Type guard
- Type guard union
- TS 2677
- A type predicate's type must be assignable to its parameter's type
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about
- Tested on (next, 5.3.2, 4.9.5, 3.9.7)
⏯ Playground Link
💻 Code
// U must extend T, else we get TS2766 error "A type predicate's type must be assignable to its parameter's type."
type UnaryTypeGuard<T, U extends T = T> = (arg: T) => arg is U;
// Let's decalre some guard-signatured function types
declare type Guard1 = (x: 1 | 2 | 3) => x is 1;
declare type Guard2 = (x: 1 | 2 | 3) => x is 2;
declare type Guard3 = (x: 2 | 3 | 4) => x is 4;
// Typed as ["Guard with types:", 1 | 2 | 3, 1] as expected
type TestGuard1 = Guard1 extends UnaryTypeGuard<infer T, infer U> ? ["Guard with types:", T, U] : "Not Guard";
// Typed as ["Guard with types:", 1 | 2 | 3, 1 | 2] as expected
type TestUnion12 = Guard1 | Guard2 extends UnaryTypeGuard<infer T, infer U> ? ["Guard with types:", T, U] : "Not Guard";
// Typed as ["Guard with types:", any, 2 | 4], so `Guard2 | Guard3` matches type guard signature
type TestUnion23Any = Guard2 | Guard3 extends UnaryTypeGuard<any, infer U> ? ["Guard with types:", any, U] : "Not Guard";
// But this one is typed as "Not Guard", which means `Guard2 | Guard3` doesn't match type guard signature...
type TestUnion23 = Guard2 | Guard3 extends UnaryTypeGuard<infer T, infer U> ? ["Guard with types:", T, U] : "Not Guard";
// Let's introduce a function with signature of unioned guards
declare const oneOf: Guard2 | Guard3;
declare const a: 2 | 3;
// While hovering the function call you can see
// `const oneOf: (x: 2 | 3) => x is 2 | 4`
// which seems a type violating TS2766,
// but we still can narrow types as expected
if (oneOf(a)) {
// `a` is definitely of type `2` here
} else {
// `a` is definitely of type `3` here
}
// We can retrieve this stored type `4` in such case
declare const b: any;
if (oneOf(b)) {
// `b is `2 | 4`
}
🙁 Actual behavior
A union type of user-defined guards' types produces an incorrect guard signature which leads to inconsistent behaviour. The resulting type doesn't extends the type guard sinature in general, but a value of such type acts as a type guard.
// Typed as "Not Guard"
type TestUnion23 = Guard2 | Guard3 extends UnaryTypeGuard<infer T, infer U> ? ["Guard with types:", T, U] : "Not Guard";
// typed as `const oneOf: (x: 2 | 3) => x is 2 | 4` which violates TS2677
decalre const oneOf: Guard2 | Guard3;
🙂 Expected behavior
A union type of user-defined guards' types should produce a correct type guard signature.
A possible way to achieve this is to forcibly intersect unioned predicates' types with intersected arguments' types to compute a resulting predicate's type. Then:
// ["Guard with types:", 2 | 3, 2]
type TestUnion23 = Guard2 | Guard3 extends UnaryTypeGuard<infer T, infer U> ? ["Guard with types:", T, U] : "Not Guard";
// typed as `const oneOf: (x: 2 | 3) => x is 2` which doesn't violates TS2677
decalre const oneOf: Guard2 | Guard3;
Additional information about the issue
This report comes from my original task of typing a generic function which combines several user-defined type guards with OR strategy.
The simplest solution (see below) spawns the reported behaviour.
function someGuard<TGuards extends UnaryTypeGuard<any>[]>(...guards: TGuards): TGuards[number] {
return (x => guards.some(g => g(x))) as TGuards[number];
}
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Direzione di ricerca
Inizia con l’esempio TypeScript Playground collegato e le dichiarazioni Guard1, Guard2, Guard3, UnaryTypeGuard e someGuard. Riproduci il comportamento delle unioni nelle versioni di TypeScript elencate, quindi traccia il modo in cui i predicati definiti dall’utente riuniti vengono inferiti e convalidati. Il lavoro è completato quando il predicato risultante rimane assegnabile al proprio tipo di parametro e i controlli dei tipi condizionali producono i risultati attesi.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- typescript
- Ambito
- compilers
- Tipo di issue
- Bug
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Stato di attività
- Ferma
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 30/100