Overview
Name | Description | Syntax |
---|---|---|
typeof | Obtains the type of a variable | type XType = typeof x |
keyof | Obtains the keys (property names) of a type. | type PersonKeys = keyof Person |
Mapped Types | Allows creating new types based on the properties of existing types | type Optional<T> = {[K in keyof T]?: T[K]} |
Conditional Types | Allows expressing a type based on a condition | type TypeName<T> = T extends string ? 'string' : 'non-string' |
Typeof
You will often be using typeof
and keyof
operators in combination, typeof
to get the type of an object and keyof
to index it.
In JavaScript (and TypeScript), “typeof” is a type-checking operator. It returns a string indicating the type of whatever operand you pass to it.
Extends typeof
TypeScript also extends it to be usable in a type context to get the TypeScript type of a variable.
type ShoppingList = {
fruitAndVegetables: string[];
meat: string[];
dairy: string[]; //etc
};
const shoppingList: ShoppingList = {
fruitAndVegetables: [],
meat: [],
dairy: [],
};
console.log('Shopping List:', typeof shoppingList);
//Shopping List: object
type ShoppingList2 = typeof shoppingList;
/*
type ShoppingList2 = {
fruitAndVegetables: string[];
meat: string[];
dairy: string[];
}
*/
Use for narrowing
We’ll deal with the first case, strings:
function concat(a: string | unknown[], b: string | unknown[]) {
if (typeof a === 'string' && typeof b === 'string') {
return a + b;
}
}
TypeScript is smart enough to use the typeof operator here to know if we’re inside that if statement, then a and b must have been strings.
keyof
The keyof
operator is introduced by typescript. It gives the properties of an object type in form of a union. For example:
type X = {a: number; b: boolean;};
const x: X = {a: 1, b: false};
// Y = 'a' | 'b'
type Y = keyof X;
const y: Y = 'a';
/* Without explicitly specifying y as type Y TS will infer its type as string and will throw an error about indexing type X using string */
console.log(x[y]);
Indexed Access Types
We can use an indexed access type to look up a specific property on another type:
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // type Age = number
Mapped Types
What is Mapped Types
Mapped types build on the syntax for index signatures, which are used to declare the types of properties which have not been declared ahead of time:
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
};
Also you can use generic type to iterate through keys to create a type:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
The type OptionsFlags
will take all the properties from the type Type
and change their values to be a boolean.
Mapping Modifiers
Two additional modifiers: readonly
and ?
affect mutability and optionality respectively. You can remove or add these modifiers by prefixing with -
or +
.
Key Remapping via as
You can leverage features like template literal types to create new property names from prior ones:
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
Conditional Types
Conditional types take a form that looks a little like conditional expressions (condition ? trueExpression : falseExpression
) in JavaScript: SomeType extends OtherType ? TrueType : FalseType;
Example:
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string; // type Example1 = number
type Example2 = RegExp extends Animal ? number : string; // type Example2 = string
Template Literal Types
Template literal types build on string literal types, and have the ability to expand into many strings via unions.
They have the same syntax as template literal strings in JavaScript, but are used in type positions. When used with concrete literal types, a template literal produces a new string literal type by concatenating the contents.
type World = "world";
type Greeting = `hello ${World}`; // Now: type Greeting = "hello world"
satisfies Operator
You may faced with a dilemma: we want to ensure that some expression matches some type, but also want to keep the most specific type of that expression for inference purposes.
That means: You don’t have to use type annotation to define a variable in order to make it matches specific type. When you use type annotation to define a variable, this variable will be taken as the specific type. At this point, this variable will under the constraint of the specific type. In some case, you may just want to make sure the variable in the shape of specific type but don’t want the constraint. Then use satisfies
operatior.
Not care about property name, Just care about the type of property
Maybe we don’t care about if the property names match up somehow, but we do care about the types of each property. In that case, we can also ensure that all of an object’s property values conform to some type. Like this:
type RGB = [red: number, green: number, blue: number];
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0] // will catch this error!
} satisfies Record<string, string | RGB>;
// Information about each property is still maintained.
const redComponent = palette.red.at(0); // this still work
const greenNormalized = palette.green.toUpperCase(); // this still work
ensure that an object has all the keys of some type, but no more:
type Colors = "red" | "green" | "blue";
// Ensure that we have exactly the keys from 'Colors'.
const favoriteColors = {
"red": "yes",
"green": false,
"blue": "kinda",
"platypus": false // ~~~~~~~~~~ error - "platypus" was never listed in 'Colors'.
} satisfies Record<Colors, unknown>;
// All the information about the 'red', 'green', and 'blue' properties are retained.
const g: boolean = favoriteColors.green;