I recently came across a good TS interview question and would like to share it.
This question has 3 levels, let’s look at them one by one.
The requirements for the first level are as follows:
Implement a zip function to merge the elements of two arrays in order, such as input [1,2,3] , [4,5,6], return [[1,4], [2,5],[3,6]]
This layer is to fetch from two arrays each time After merging an element, put it into the array, and then continue processing the next one, and continue this process recursively until the array is empty.
function zip(target, source) { if (!target.length || !source.length) return []; const [one, ...rest1] = target; const [other, ...rest2] = source; return [[one, other], ...zip(rest1, rest2)]; }
The result is correct:
The first level is relatively simple, then let’s look at the second level requirements:
Define the ts type for this zip function (two writing methods)
There are two forms of function definition:
Declare the function directly through function:
function func() {}
And declare an anonymous function and assign it to a variable:
const func = () => {}
The types of parameters and return values are both arrays, but the specific types are not known, so you can write unknown[].
So the definition of the two function types is like this:
is also a direct function declaration of function type and interface declaration The function type is then added to the variable type both ways.
Because the specific element type is not known, unknown is used.
You may ask here the difference between any and unknown:
Both any and unknown can receive any type:
But any can also Assign to any type, but unknown.
This is only used to receive other types, so unknown is more appropriate and safer than any.
This level is also relatively basic ts syntax, and the third level becomes more difficult:
Use type programming to achieve precise type hints, such as passing parameters in [1, 2,3], [4,5,6], then the type of the return value should be prompted as [[1,4], [2,5],[3,6]]
here If the return value type is required to be precise, we must dynamically generate the return value type based on the type of the parameter.
That's it:
Declare two type parameters Target and Source, and the constraint is unknown[], which is an array type of any element type.
These two type parameters are the types of the two parameters passed in.
The return value is calculated by Zip.
Then we need to implement the advanced type of Zip:
The type parameters passed in are two array types, and we also need to extract each element from them and merge them together.
Pattern matching can be used to extract elements:
So this type can be defined like this:
type Zip<One extends unknown[], Other extends unknown[]> = One extends [infer OneFirst,...infer Rest1] ? Other extends [infer OtherFirst, ...infer Rest2] ? [[OneFirst, OtherFirst], ...Zip<Rest1, Rest2>] : [] : [];
Extract the first elements of the two arrays respectively and construct a new array. Then do this recursively for the remaining array until the array is empty.
This achieves the advanced type we want:
#But if you add it as a return value to the function, an error will be reported:
Because we don’t know what the parameters are when we declare the function, we naturally cannot calculate the value of Zip
then what should we do?
can be solved by function overloading:
#ts supports function overloading. You can write the type definition of multiple types of functions with the same name, and finally write the function's Implementation, so that when this function is used, the function type will be matched according to the type of the parameter.
The function we use type programming will not report an error if it is written in this way.
Let’s take a look:
Why is the return value type wrong?
#In fact, the matching function type is correct at this time, but the deduced type is not a literal type.
You can add as const at this time.
But adding as const will deduce readonly [1,2,3]
This type is not It matches, so we need to add readonly:
to the declaration of the type parameter, but the type of the Zip function does not match again.
Should we add readonly to all places where this type is used?
No need, can we just remove the readonly modification?
Typescript has a built-in advanced type readonly:
You can add readonly modification to each index of the index type:
But there is no advanced type that removes the readonly modification. We can implement it ourselves:
Use the mapping type syntax to construct a new index type. Adding -readonly means removing the readonly modification.
#Some students may ask, is the array type also an index type?
Yes, the index type is a type that aggregates multiple elements, so objects, arrays, and classes are all.
So we can naturally use it on arrays:
(To be precise, it’s called a tuple. A tuple has a fixed number of elements. Array)
Then we only need to use Mutable to remove readonly before passing in the Zip:
Let’s try again:
Done! Now the return value type is correct.
But there is still a problem. If the literal is not passed in directly, the literal type cannot be deduced. At this time, something seems wrong:
But don’t we all declare overloaded types?
If the literal type cannot be deduced, it should match this:
But in fact it matches the first one:
At this time, you only need to change the order of the two function types:
At this time, the situation of literal parameters is still correct:
Why?
Because the types of overloaded functions are matched from top to bottom, as long as one is matched, it will be applied.
In the case of non-literal values, the type is number[], which can match the type of unknown[], so that function type takes effect.
In the case of literals, the derivation is readonly [1,2,3], with readonly so it does not match unknown[], and continues to match, just The function type with type parameters was matched.
In this way, the appropriate function type is applied in both cases.
The whole code is like this:
type Zip<One extends unknown[], Other extends unknown[]> = One extends [ infer OneFirst, ...infer Rest1 ] ? Other extends [infer OtherFirst, ...infer Rest2] ? [[OneFirst, OtherFirst], ...Zip<Rest1, Rest2>] : [] : []; type Mutable<Obj> = { -readonly [Key in keyof Obj]: Obj[Key]; }; function zip(target: unknown[], source: unknown[]): unknown[]; function zip<Target extends readonly unknown[], Source extends readonly unknown[]>( target: Target, source: Source ): Zip<Mutable<Target>, Mutable<Source>>; function zip(target: unknown[], source: unknown[]) { if (!target.length || !source.length) return []; const [one, ...rest1] = target; const [other, ...rest2] = source; return [[one, other], ...zip(rest1, rest2)]; } const result = zip([1, 2, 3] as const, [4, 5, 6] as const); const arr1 = [1, 2, 3]; const arr2 = [4, '5', 6]; const result2 = zip(arr1, arr2);
今天我们做了一道综合的 ts 面试题,一共有三层:
第一层实现 js 的逻辑,用递归或者循环都能实现。
第二层给函数加上类型,用 function 声明类型和 interface 声明函数类型两种方式,参数和返回值都是 unknown[]。
第三层是用类型编程实现精准的类型提示,这一层需要拿到参数的类型,通过提取元素的类型并构造出新的数组类型返回。还要通过函数重载的方式来声明类型,并且要注意重载类型的声明顺序。
as const 能够让字面量推导出字面量类型,但会带有 readonly 修饰,可以自己写映射类型来去掉这个修饰。
其实这也是我们学习 ts 的顺序,我们先要能把 js 逻辑写出来,然后知道怎么给函数、class 等加 ts 类型,之后学习类型编程,知道怎么动态生成类型。
其中类型编程是 ts 最难的部分,也是最强大的部分。攻克了这一层,ts 就可以说学的差不多了。
【相关推荐:javascript学习教程
The above is the detailed content of Share a good TS interview question (including 3 levels) and see which level you can answer!. For more information, please follow other related articles on the PHP Chinese website!