Type Constraint extends#
Syntax: T extends K
, where extends
is not inheritance for classes or interfaces, but rather a way to judge and constrain types. It means to determine if T
can be assigned to K
.
Determine if T
can be assigned to U
, if so, return T
, otherwise return never
:
type Exclude<T, U> = T extends U ? T : never
Type Mapping in#
Iterate over the keys of a specified interface or a union type:
interface Person {
name: string
age: number
gender: number
}
// Convert all properties of T to readonly type
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
// type ReadonlyPerson = {
// readonly name: string
// readonly age: number
// readonly gender: number
// }
type ReadonlyPerson = Readonly<Person>
Type Predicate is#
TypeScript has a type guard mechanism. To define a type guard, we simply define a function that returns a type predicate:
function isString(test: any): test is string {
return typeof test === 'string'
}
What is the difference between the above code and a function that returns a boolean value?
function isString(test: any): boolean {
return typeof test === 'string'
}
When using the is
type guard:
function isString(test: any): test is string {
return typeof test === 'string'
}
function example(foo: any) {
if (isString(foo)) {
console.log('it is a string' + foo)
console.log(foo.length) // string function
// The following code will cause a compilation error and a runtime error because foo is a string and does not have the toExponential method
console.log(foo.toExponential(2))
}
// Compilation will not fail, but a runtime error will occur
console.log(foo.toExponential(2))
}
example('hello world')
When the return value is boolean:
function isString(test: any): boolean {
return typeof test === 'string'
}
function example(foo: any) {
if (isString(foo)) {
console.log('it is a string' + foo)
console.log(foo.length) // string function
// foo is of type any, so it compiles fine. But a runtime error will occur because foo is a string and does not have the toExponential method
console.log(foo.toExponential(2))
}
}
example('hello world')
Summary:
- When using type guards, TS further narrows down the type of the variable. In the example, the type is narrowed down from any to string;
- The scope of a type guard is limited to the block scope after the if statement.
Practical example:
function isAxiosError(error: any): error is AxiosError {
return error.isAxiosError
}
if (isAxiosError(err)) {
code = `Axios-${err.code}`
}
Inferred Type infer#
You can use infer P
to mark a generic type as an inferred type and use it directly.
Get the parameter type of a function:
type ParamType<T> = T extends (param: infer P) => any ? P : T
type FunctionType = (value: number) => boolean
type Param = ParamType<FunctionType> // type Param = number
type OtherParam = ParamType<symbol> // type Param = symbol
Determine if T
can be assigned to (param: infer P) => any
, and infer the parameter as the generic type P
. If it can be assigned, return the parameter type P
, otherwise return the input type.
Get the return type of a function:
type ReturnValueType<T> = T extends (param: any) => infer U ? U : T
type FunctionType = (value: number) => boolean
type Return = ReturnValueType<FunctionType> // type Return = boolean
type OtherReturn = ReturnValueType<number> // type OtherReturn = number
Determine if T
can be assigned to (param: any) => infer U
, and infer the return type as the generic type U
. If it can be assigned, return the return type P
, otherwise return the input type.
Primitive Type Guard typeof#
Syntax: typeof v === 'typename'
or typeof v !== 'typename'
, used to determine if the type of data is a specific primitive type (number
, string
, boolean
, symbol
) and perform type guarding.
The typename must be number
, string
, boolean
, or symbol
. However, TypeScript does not prevent you from comparing it with other strings, as the language does not recognize those expressions as type guards.
Example: The print function will print different results based on the parameter type. How do we determine if the parameter is a string
or a number
?
function print(value: number | string) {
// If it is a string type
// console.log(value.split('').join(', '))
// If it is a number type
// console.log(value.toFixed(2))
}
There are two common ways to determine the type:
- Determine if it has the
split
property to check if it is astring
type, and if it has thetoFixed
method to check if it is anumber
type. Drawback: Type conversion is required for both checking and calling. - Use a type predicate
is
. Drawback: You have to write a utility function every time, which is too cumbersome.
In this case, we can use typeof
:
function print(value: number | string) {
if (typeof value === 'string') {
console.log(value.split('').join(', '))
} else {
console.log(value.toFixed(2))
}
}
After using typeof
for type checking, TypeScript narrows down the variable to that specific type, as long as the type is compatible with the original type of the variable.
Type Guard instanceof#
Similar to typeof
, but works differently. The instanceof
type guard is a way to refine types based on their constructor functions.
The right-hand side of instanceof
must be a constructor function. TypeScript refines it to:
- The type of the
prototype
property of this constructor function, if its type is notany
- The union of the types returned by the construct signatures
class Bird {
fly() {
console.log('Bird flying')
}
layEggs() {
console.log('Bird layEggs')
}
}
class Fish {
swim() {
console.log('Fish swimming')
}
layEggs() {
console.log('Fish layEggs')
}
}
const bird = new Bird()
const fish = new Fish()
function start(pet: Bird | Fish) {
// Calling layEggs is fine because both Bird and Fish have the layEggs method
pet.layEggs()
if (pet instanceof Bird) {
pet.fly()
} else {
pet.swim()
}
// Equivalent to the following
// if ((pet as Bird).fly) {
// (pet as Bird).fly();
// } else if ((pet as Fish).swim) {
// (pet as Fish).swim();
// }
}
Index Type Query Operator keyof#
Syntax: keyof T
, for any type T
, the result of keyof T
is the union of known public property names on T
.
keyof
is similar to Object.keys
, but keyof
takes the keys of an interface.
interface Point {
x: number
y: number
}
// type keys = "x" | "y"
type keys = keyof Point
Suppose there is an object as shown below, and we need to implement a get
function using TypeScript to get its property value:
function get(o: object, name: string) {
return o[name]
}
We might start by writing it like this, but it has many drawbacks:
- Unable to determine the return type: This will lose TypeScript's most powerful type checking feature;
- Unable to constrain the key: It may cause spelling errors.
In this case, we can use keyof
to enhance the type functionality of the get
function:
function get<T extends object, K extends keyof T>(o: T, name: K): T[K] {
return o[name]
}
Note that keyof
can only return known public property names on a type:
class Animal {
type: string
weight: number
private speed: number
}
type AnimalProps = keyof Animal // 'type' | 'weight'
When you need to get the value of a specific property of an object, but you are not sure which property it is, you can use extends
with typeof
to restrict the parameter to be a property name of the object:
const person = {
name: 'Jack',
age: 20,
}
function getPersonValue<T extends keyof typeof person>(fieldName: keyof typeof person) {
return person[fieldName]
}
const nameValue = getPersonValue('name')
const ageValue = getPersonValue('age')