The best way to solve the TS problem is to practice more. This time, the difficulty of interpreting type-challenges Medium is 41~48 questions.
intensive reading
ObjectEntries
Implement the TS version of Object.entries
:
interface Model {
name: string;
age: number;
locations: string[] | null;
}
type modelEntries = ObjectEntries<Model> // ['name', string] | ['age', number] | ['locations', string[] | null];
After the previous foreshadowing, everyone should be familiar with the TS thinking problem. The first thought after seeing this question should be: How to convert the object into a union type first? If this problem is not solved, there is no way to start.
The idea of object or array to union type is similar. An array to union type uses [number]
as the subscript:
['1', '2', '3']['number'] // '1' | '2' | '3'
The way of the object is [keyof T]
as a subscript:
type ObjectToUnion<T> = T[keyof T]
Looking at this question again, each item of the union type is an array, namely Key and Value, which is easier to write. We only need to construct an object whose Value is in line with the structure:
type ObjectEntries<T> = {
[K in keyof T]: [K, T[K]]
}[keyof T]
In order to pass the single test ObjectEntries<{ key?: undefined }>
and make the Key position not appear undefined
, it is necessary to force the object to be described as a non-optional Key:
type ObjectEntries<T> = {
[K in keyof T]-?: [K, T[K]]
}[keyof T]
In order to pass the single test ObjectEntries<Partial<Model>>
, the Value undefined
must be removed:
// 本题答案
type RemoveUndefined<T> = [T] extends [undefined] ? T : Exclude<T, undefined>
type ObjectEntries<T> = {
[K in keyof T]-?: [K, RemoveUndefined<T[K]>]
}[keyof T]
Shift
Implement TS version Array.shift
:
type Result = Shift<[3, 2, 1]> // [2, 1]
This question should be easy and difficult, just discard the first item, and use infer
to easily achieve:
// 本题答案
type Shift<T> = T extends [infer First, ...infer Rest] ? Rest : never
Tuple to Nested Object
Implements TupleToNestedObject<T, P>
, where T
only accepts string arrays, P
is any type, and generates a recursive object structure that satisfies the following results:
type a = TupleToNestedObject<['a'], string> // {a: string}
type b = TupleToNestedObject<['a', 'b'], number> // {a: {b: number}}
type c = TupleToNestedObject<[], boolean> // boolean. if the tuple is empty, just return the U type
This question uses 5 knowledge points: recursion, auxiliary types, infer
, how to specify the object Key, PropertyKey
, you have to know all of them and combine them to solve the problem.
First of all, because the return value is a recursive object, it must be constantly modified during the recursive process, so add a third parameter to the generic type R
to store this object, and start from the last one when recursing the array, so from the innermost The layer object starts to "wrap" it a little bit:
type TupleToNestedObject<T, U, R = U> = /** 伪代码
T extends [...infer Rest, infer Last]
*/
The next step is how to describe an object Key? Before Chainable Options
example we learned K in Q
, but we need to note that writing this directly will report an error, because we must declare Q extends PropertyKey
. Finally, process the recursive end condition, that is, T
returns directly when it becomes an empty array R
:
// 本题答案
type TupleToNestedObject<T, U, R = U> = T extends [] ? R : (
T extends [...infer Rest, infer Last extends PropertyKey] ? (
TupleToNestedObject<Rest, U, {
[P in Last]: R
}>
) : never
)
Reverse
Implement TS version Array.reverse
:
type a = Reverse<['a', 'b']> // ['b', 'a']
type b = Reverse<['a', 'b', 'c']> // ['c', 'b', 'a']
This problem is simpler than the previous one, just use a recursion:
// 本题答案
type Reverse<T extends any[]> = T extends [...infer Rest, infer End] ? [End, ...Reverse<Rest>] : T
Flip Arguments
Implement FlipArguments<T>
to reverse the parameters of the function T
:
type Flipped = FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void>
// (arg0: boolean, arg1: number, arg2: string) => void
This question is similar to the previous question, except that the content is reversed from an array to a parameter of a function. Just use infer
to define the parameters of the function, and use the Reverse
function to reverse it:
// 本题答案
type Reverse<T extends any[]> = T extends [...infer Rest, infer End] ? [End, ...Reverse<Rest>] : T
type FlipArguments<T> =
T extends (...args: infer Args) => infer Result ? (...args: Reverse<Args>) => Result : never
FlattenDepth
Implement Flatten with specified depth:
type a = FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2> // [1, 2, 3, 4, [5]]. flattern 2 times
type b = FlattenDepth<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, [[5]]]. Depth defaults to be 1
This question is a bit trickier than the previous one Flatten
because it needs to control the number of draws.
The basic idea is that the leveling is Deep
times, so it is necessary to implement the function of leveling once, and then recurse corresponding times according to the Deep
value:
type FlattenOnce<T extends any[], U extends any[] = []> = T extends [infer X, ...infer Y] ? (
X extends any[] ? FlattenOnce<Y, [...U, ...X]> : FlattenOnce<Y, [...U, X]>
) : U
Then implement the main function FlattenDepth
, because TS cannot implement + and - operations, we must use the array length judgment and operation array to assist in the realization:
// FlattenOnce
type FlattenDepth<
T extends any[],
U extends number = 1,
P extends any[] = []
> = P['length'] extends U ? T : (
FlattenDepth<FlattenOnce<T>, U, [...P, any]>
)
When the recursion does not reach the depth U
, use the method of [...P, any]
to plug an element into the array. Next time, if it can match P['length'] extends U
means that the recursion depth has been reached.
However, considering that the test case FlattenDepth<[1, [2, [3, [4, [5]]]]], 19260817>
will cause a very long recursion, it needs to be terminated in advance, that is, if it is already flat after the draw, there is no need to continue the recursion. At this time, you can use FlattenOnce<T> extends T
Judgment:
// 本题答案
// FlattenOnce
type FlattenDepth<
T extends any[],
U extends number = 1,
P extends any[] = []
> = P['length'] extends U ? T : (
FlattenOnce<T> extends T ? T : (
FlattenDepth<FlattenOnce<T>, U, [...P, any]>
)
)
BEM style string
Implement the BEM
function to complete its rule splicing:
Expect<Equal<BEM<'btn', [], ['small', 'medium', 'large']>, 'btn--small' | 'btn--medium' | 'btn--large' >>,
Before we learned about converting an array or object into a union type by subscripting, there is a special case here, that is, each item declared in this way in the string will be automatically Cartesian product into a new union type:
type BEM<B extends string, E extends string[], M extends string[]> =
`${B}__${E[number]}--${M[number]}`
This is the simplest way to write it, but doesn't take into account the case where the item doesn't exist. It's better to create a SafeUnion
function that returns an empty string when the passed value does not exist to ensure safe skipping:
type IsNever<TValue> = TValue[] extends never[] ? true : false;
type SafeUnion<TUnion> = IsNever<TUnion> extends true ? "" : TUnion;
Final code:
// 本题答案
// IsNever, SafeUnion
type BEM<B extends string, E extends string[], M extends string[]> =
`${B}${SafeUnion<`__${E[number]}`>}${SafeUnion<`--${M[number]}`>}`
InorderTraversal
Implement in-order traversal of the TS version of the binary tree:
const tree1 = {
val: 1,
left: null,
right: {
val: 2,
left: {
val: 3,
left: null,
right: null,
},
right: null,
},
} as const
type A = InorderTraversal<typeof tree1> // [1, 3, 2]
First recall the implementation of the JS version of the in-order traversal of the binary tree:
function inorderTraversal(tree) {
if (!tree) return []
return [
...inorderTraversal(tree.left),
res.push(val),
...inorderTraversal(tree.right)
]
}
For TS, the way to implement recursion is a little different, that is, by extends TreeNode
to determine that it is not Null and recurse:
// 本题答案
interface TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
}
type InorderTraversal<T extends TreeNode | null> = [T] extends [TreeNode] ? (
[
...InorderTraversal<T['left']>,
T['val'],
...InorderTraversal<T['right']>
]
): []
You may ask, why can't you use null
to make judgments like JS?
type InorderTraversal<T extends TreeNode | null> = [T] extends [null] ? [] : (
[ // error
...InorderTraversal<T['left']>,
T['val'],
...InorderTraversal<T['right']>
]
)
If you write in this way, you will find that TS throws an exception, because TS cannot determine T
at this time, it matches the type of TreeNode
, so the forward judgment is generally used when performing operations.
Summarize
These types of challenges require a flexible combination of the basic knowledge points of TS to be cracked. Commonly used ones include:
- How to operate objects, increase or decrease Key, read-only, merge into one object, etc.
- recursion, and helper types.
-
infer
knowledge point. - Union types, how to generate union types from objects or arrays, the relationship between string templates and union types.
The discussion address is: Intensive Reading "ObjectEntries, Shift, Reverse..." Issue #431 dt-fe/weekly
If you'd like to join the discussion, click here , there are new topics every week, with a weekend or Monday release. Front-end intensive reading - help you filter reliable content.
Follow Front-end Intensive Reading WeChat Official Account
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
Copyright notice: Free to reprint - non-commercial - non-derivative - keep attribution ( Creative Commons 3.0 license )
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。