Advanced TypeScript Exercises
If we have a type which is wrapped type like Promise. How we can get a type which is inside the wrapped type? For example if we have Promise<ExampleType>
how to get ExampleType?
Take a look at below code. Write an utility type Transform which will take a generic type argument, and if it is a Promise it will evaluate to the type inside it.
type X = Promise<string>
type Y = Promise<{ field: number }>
type ResultX = Transform<X>; // ResultX type equals string
type ResultY = Transform<Y>; // ResultY type equals { field: number }
/** here your answer **/
type Transform<A> = A extends Promise<infer Inner> ? Inner : never
Understanding infer
We can use infer also with other types, consider example with Array:
type InsideArray<A> = A extends Array<infer Inside> ? Inside : never
// Str is type string
type Str = InsideArray<Array<string>>
custom parameterized types?
type Supprise<A> = { inside: A }
type UnpackSupprise<S> = S extends Supprise<infer Inside> ? Inside : never
// Num is type number
type Num = UnpackSupprise<Supprise<number>>
We can even use infer to get mapped types properties
type User = {
id: number,
name: string,
}
type Doc = {
id: string,
}
type GetProperty<T, Prop extends keyof T> = T extends { [K in Prop]: infer Value } ? Value : never
type UserId = GetProperty<User, 'id'>
type DocId = GetProperty<Doc, 'id'>
Convert to tuple
type ABC<A, B, C> = { a: A, b: B, c: C }
type ABCIntoTuple<T>
= T extends ABC<infer A, infer B, infer C> ? [A, B, C] : never
type Example = ABC<string, boolean, number>
type ExampleTuple = ABCIntoTuple<Example>
Part 2
In this question I will ask you, why TS fails here. And I can say there is a valid reason why such construct is wrong, its not a language bug. Can you spot why, and what is example type which proves TypeScript rightly prevents such code to compile?
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): T {
// Below error, why?
return {
id: u.id,
kind: 'customer'
}
}
when extends
only extends
At the first look object which is returned by makeCustomer
is valid User
type as it has both needed fields defined in the User
. The crucial thing to understand is that we work here with type variable T
which extends from User
but it doesn't mean it is User
. T
is assignable to User
, so it needs to have all fields which User
has, but, it can have more fields!
Yes and this is exactly the issue, returned object is a User
and pass all constraints of it, but doesn't pass all constraints of T
which can have additional fields. We don't know what are those fields though, so in order to fix the typing we should make an object which has all fields of T
, and we know all fields of T
are in argument u
. We can then use spread operator in order to spread all unknown fields to the newly created object.
function makeCustomer<T extends User>(u: T): T {
// no error
return {
...u, // spread all properties of u being T
id: u.id, // yes redundant line, leaving it for consistency
kind: 'customer'
}
}
Part 3
同步参数类型
Today's question is about typing function with two arguments being union type. The goal is to block possibility to pass mixed types into arguments, so if the first argument is a number
then second also needs to be number
, in other words there is dependency between arguments which we need to write.
function f(a: string | number, b: string | number) {
if (typeof a === 'string') {
return a + ':' + b; // no error but b can be number!
} else {
return a + b; // error as b can be number | string
}
}
f(2, 3); // correct usage
f(1, 'a'); // should be error
f('a', 2); // should be error
f('a', 'b') // correct usage
解法一最简单:
function f(T extends string | number)(a: T, b: T) {
}