Conditional types
💻Programming Language
javascript
typescript
About conditional types
- Conditional types involve
extendsand the ternary operator. -
They contain type variables. Type variables may be defined:
- after
inferwithin theextendsclause of a conditional type →RinT extends (...args: any[]) => infer R - on the left-hand side of the type declaration →
TinReturnType<T> = ... - as part of a mapped type →
Kintype Mapped<T> = { [K in keyof T]: ... }
(taken from this SO answer)
- These are all declared type variables.
- If you want to introduce new type variables in the scope of a conditional type, you have to use
inferto declare them.
- after
Example: Remove certain member from union type
type Animal = Lion | Zebra | Tiger | Shark
type ExtractCat<A> = A extends { meow(): void } ? A : never
type Cat = ExtractCat<Animal>
// => Lion | Tigertype ExcludeTypeKey<K> = K extends "type" ? never : K
type Test = ExcludeTypeKey<"emailAddress" | "type" | "foo">
// => "emailAddress" | "foo"Wonderful article about conditional types: https://artsy.github.io/blog/2018/11/21/conditional-types-in-typescript/
Example where conditional types are more complicated
type ExtractId<Action, T> = Action extends 'delete' | 'put' ? T : never;
const generateUrl = <Action extends CrudAction>(
crudAction: Action,
entity: Entity,
id?: ExtractId<Action, number | string>
): string => {
switch (crudAction) {
case 'delete':
case 'put':
return `${baseUrl}/${entity}/${id}`;
default:
case 'post':
case 'get':
return `${baseUrl}/${entity}`;
}
};
generateUrl('post', 'inhaler'); // ✅ Good!
generateUrl('post', 'inhaler', 6); // ✅ Good!
generateUrl('put', 'inhaler'); // ❌ This shouldn't be allowed! Third argument `id` is required!Solution with function overloading:
interface GenerateUrl {
(crudAction: 'delete' | 'put', entity: Entity, id: number | string): string;
(crudAction: 'get' | 'post', entity: Entity, id?: never): string;
}
const generateUrl: GenerateUrl = (
crudAction: CrudAction,
entity: Entity,
id?: number | string
): string => {
switch (crudAction) {
case 'delete':
case 'put':
return `${baseUrl}/${entity}/${id}`;
default:
case 'post':
case 'get~':
return `${baseUrl}/${entity}`;
}
};
generateUrl('put', 'inhaler'); // ✅ Error as desired!Now I get error: Argument of type '"put"' is not assignable to parameter of type '"get" | "post"'
Issue remains when calling it with general CrudAction:
const url = generateUrl(crudAction, entity, data.id);Conditional type with many conditions
AnimateStyle taken from node_modules/react-native-reanimated/react-native-reanimated.d.ts:
export type AnimateStyle<S> = {
[K in keyof S]: K extends 'transform'
? AnimatedTransform
: S[K] extends ReadonlyArray<any>
? ReadonlyArray<AnimateStyle<S[K][0]>>
: S[K] extends object
? AnimateStyle<S[K]>
: S[K] extends ColorValue | undefined
? S[K] | number
:
| S[K]
| AnimatedNode<
// allow `number` where `string` normally is to support colors
S[K] extends ColorValue | undefined ? S[K] | number : S[K]
>;
};Conditional type which checks for a valid value given an object’s key
type ObjT = {
foo: 1;
bar: 2;
}
// Conditional type which checks for a valid value given an object's key
type ObjectValue<Obj extends object, Key extends string> = Key extends keyof Obj ? Obj[Key] : never;
// Works:
const myString1: ObjectValue<ObjT, 'foo' | 'bar'> = 1;
// Type error because `3` is not a value in `ObjT`:
const myString2: ObjectValue<ObjT, 'foo' | 'bar'> = 3;Example with parameter spread: Tail of an array
/**
* [H, ...T] -> T
*/
export type Tail<L extends any[]> = ((...l: L) => any) extends ((h: any, ...t: infer T) => any) ? T : neverFound in node_modules/@redux-saga/core/types/ts3.6/effects.d.ts
Not a conditional type but also nice:
/**
* [...A, B] -> A
*/
export type AllButLast<L extends any[]> = Reverse<Tail<Reverse<L>>>Discuss on Twitter ● Improve this article: Edit on GitHub