- type guard: operator that can narrow down types, e.g.
in
, typeof
, instanceof
, comparison operators, truthiness, etc.
beware: TS doesn’t yet handle coercion, e.g. ==
might fail #37251
interface Bird {
name: string;
wingspan: number;
}
interface Fish {
name: string;
fins: number;
}
declare function getPet(): Bird | Fish
let pet = getPet();
pet.name;
pet.wingspan;
if ("wingspan" in pet) {
pet.wingspan;
} else {
pet.fins;
}
function typeSwitch(x: string | number): string {
if (typeof x == "string") {
return x.toUpperCase();
} else if (typeof x == "number") {
return x.toFixed(2);
}
throw new Error("Invalid input");
}
- use comparison operators to narrow down
null
or undefined
function stringOnly(str: string | null): string {
return str || "default";
}
- beware: doesn’t work in more complex cases like nested functions, use non-null assertion operator
!
function stringOnly(str: string | null): void {
function nested() {
return str.toUpperCase();
}
str = str || "default";
nested();
}
function stringOnly(str: string | null): void {
function nested() {
return str!.toUpperCase();
}
str = str || "default";
nested();
}
typeof
type operator
- in expression context gives JS type as string
- in type context gives TS type
let x = "Hello World!";
let y: typeof x;
Indexed type access operator
- get type of property in interface or object type
- use same bracket notation from object property accessor
- index is a literal string type, a union is distributed, i.e.
T[A | B]
is equivalent to T[A] | T[B]
type Person = {
name: string;
age: number;
}
interface Person {
name: string;
age: number;
}
type Name = Person["name"];
Index type query operator
- get name of property in interface or object type
- name is itself a literal string type or union thereof
- can then use name as index for indexed type access operator
type Person = {
name: string;
age: number;
}
type PersonKey = keyof Person;
type PersonVal = Person[PersonKey]
- can use with generic types
function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const o = { name: "Peter", age: 42 };
const name = getProperty(o, "name");
function getProperties<T, K extends keyof T>(object: T, keys: K[]): T[K][] {
return keys.map(key => object[key]);
}
const o = { name: "Peter", age: 42 };
const name = getProperties(o, ["name", "age"]);
- for object type with numeric index signature returns basic type
number
, for string index signature string | number
since since JS converts numeric indices to strings
Mapped types
- transform object type
- can modify property types, add new ones, remove existing ones, etc.
- looks like object type with index signature, but is own type, i.e. can not add more properties, need to use intersection type ❗️
type Keys = "name" | "age";
type Form = {
[K in Keys]: boolean;
};
type Form = {
name: boolean;
age: boolean;
}
- if keys are of type string or number the corresponding index signature is used
type BooleanArray = {
[K in number]: boolean;
};
const a: BooleanArray = [true, false];
- can use with generic, index type query and indexed type access operator to create powerful transformations, see Built-in generic types for many useful ones
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type Person = {
name: string,
age: number
};
type NullablePerson = Nullable<Person>;
type Nested<S> = {
value: S;
modified: boolean;
printValue(): void;
};
type Wrapper<T> = {
[P in keyof T]: Nested<T[P]>;
};
type Person = {
name: string,
age: number
};
type WrappedPerson = Wrapper<Person>;
const p: WrappedPerson = {
name: {
value: "Peter",
modified: false,
printValue() {
console.log(`My name is ${this.value}.`);
}
},
age: {
value: 42,
modified: false,
printValue() {
console.log(`I am ${this.value} years old.`);
}
},
}
p.name.printValue();
p.age.printValue();
Conditional types
- choose type based on condition
- can replace overloads with conditional type
- syntax of ternary operator in JS
T extends U ? X : Y
- can’t be resolved if condition depends on type variables that can’t be evaluated yet, is deferred, until then is carried through as is
type Return<T> = T extends string ? number : string;
declare function typeSwitch<T>(x: T): Return<T>;
const x = typeSwitch("Peter");
const y = typeSwitch(42);
type Return<T> = T extends string ? number : string;
declare function typeSwitch<T>(x: T): Return<T>;
function func<S>(x: S): void {
const y = typeSwitch(x);
}
- beware: type variables don’t get type narrowing, because are only determined at call site, needs to use type assertions, see #22735, #24929, StackOverflow
type Return<T> = T extends string ? number : T extends number ? string : never;
function typeSwitch<T extends string | number>(x: T): Return<T>{
if (typeof x == "string") {
return 42;
} else if (typeof x == "number") {
return "Hello World!";
}
throw new Error("Invalid input");
}
const x = typeSwitch("qwerty");
- conditional types distribute over union types, i.e.
A | B extends U ? X : Y
is equivalent to (A extends U ? X : Y) | (B extends U ? X : Y)
- can use distributed conditional types and
never
to filter types
type A = Exclude<"a" | "b", "a" | "c">;
type B = Extract<"a" | "b", "a" | "c">;
type C = NonNullable<"a" | null | undefined>;
- can use with mapped types to filter object type
interface Person {
name: string;
age: number;
sayHi(): void;
}
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type A = FunctionPropertyNames<Person>;
type B = FunctionProperties<Person>;
- can
infer
a type variable within the extends
clause, don’t need to declare the inferred type variable before the extends
type Flatten<T> = T extends (infer U)[] ? U : T;
type Flatten<T> = T extends any[] ? T[number] : T
type x = Flatten<string[]>;
type y = Flatten<number>;
- multiple types are inferred as a union type or intersection type depending on the context
type Props<T> = T extends { a: infer U; b: infer U } ? U : never;
type A = Props<{ a: string; b: string }>;
type B = Props<{ a: string; b: number }>;
type Args<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
type A = Args<{ a: (x: string) => void; b: (x: string) => void }>;
type B = Args<{ a: (x: string) => void; b: (x: number) => void }>;
- when inferring from a overloaded function, only the last call signature is used ❗️