- step1. ts简单实现前半部分要求
- step2. 修改默认返回类型使其为精准类型
- step3. 解决Zip<Target, Source>类型不匹配的问题
- step4. 实现去除 readonly 的高级类型
- step5. 最终
使用 ts 实现一个 zip 函数,对两个数组的元素按顺序两两合并,比如输入[1,2,3],[4,5,6]时,返回[[1,4],[2,5],[3,6]]。要求使用ts实现,并且要求用类型编程实现精确的类型提示,比如参数输入[1,2,3],[4,5,6],那返回值的类型要提示出[[1,4],[2,5],[3,6]]
上述题目如果要用js去解,或者ts去解但是没有后半部分的精确提示要求,其实是很简单的,我们可以很快写出对应代码。
step1. ts简单实现前半部分要求
// 第一种:function函数声明
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)]
}
// 第二种:匿名函数加函数的构造签名
interface Zip {
(target: unknown[], source: unknown[]): unknown[]
}
const zip: 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)]
}
step2. 修改默认返回类型使其为精准类型
这里要求返回值类型是精确的,我们就要根据参数的类型来动态生成返回值类型。
也就是这样:
function zip<Target extends unknown[], Source extends unknown[]>(target: Target, source: Source): Zip<Target, Source> {
if (!target.length || !source.length) return []
const [one, ...rest1] = target
const [other, ...rest2] = source
return [[one,other], ...zip(rest1, rest2)]
}
type Zip<Target, Source> = {
// 待补充
}
Zip类型里面的具体逻辑,也就是我们需要的精确返回类型,需要对传入进来的泛型参数里面的元素进行提取,然后组合成新类型返回。
提取元素可以用模式匹配的方式:
所以Zip类型可以这样定义:
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>]
: []
: []
;
到此,已经实现了大部分想要的功能,代码如下:
function zip<Target extends unknown[], Source extends unknown[]>(target: Target, source: Source): Zip<Target, Source> {
if (!target.length || !source.length) return []
const [one, ...rest1] = target
const [other, ...rest2] = source
return [[one,other], ...zip(rest1, rest2)]
}
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>]
: []
: []
;
zip([1,2,3], [5,6,7])
但是发现,函数仍然有类型错误提示,
这是因为声明函数的时候都不知道参数是啥,自然计算不出 Zip<Target, Source> 的值,所以这里会类型不匹配:
step3. 解决Zip<Target, Source>类型不匹配的问题
在ts里,可以使用函数重载的方法解决类似这种问题。
完善代码如下:
function zip(target: unknown[], source: unknown[]): unknown[]
function zip<Target extends unknown[], Source extends unknown[]>(target: Target, source: Source): Zip<Target, 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)]
}
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>]
: []
: []
;
const res = zip([1,2,3], [5,6,7])
代码完善后,确实没有报错了,但是返回的ts类型好像不是我们想要的。
其实, 这时候匹配的函数类型是对的,只不过推导出的不是字面量类型。 这时候可以加个 as const。
as const 的作用: TS 3.4中引入 as const,被称为const断言。 const断言告诉编译器为表达式推断出它能推断出的最窄或最特定的类型。如果不使用它,编译器将使用其默认类型推断行为,这可能会导致更广泛或更一般的类型。
看下面这个例子,
const args = [8, 5];
// const args: number[]
const angle = Math.atan2(...args); // error! Expected 2 arguments, but got 0 or more.
console.log(angle);
以上代码在js环境中执行没有任何问题,但是在ts环境中,执行Math.atan2这块会报参数错误。 编译器看到const args = [8,5];推断出args的类型是number[],这是一个由0个或更多number类型的元素组成的可变数组。但是,Math.atan2方法,只需要两个数值参数,但是编译器只知道args是一个数字数组,所以在调用Math.atan2方法时编译器会报出一个错误。 这时候,如果用到as const,就会避免上述问题。
const args = [8, 5] as const;
// const args: readonly [8, 5]
const angle = Math.atan2(...args);
console.log(angle);
使用const断言,将args断言成 readonly[8,5]类型的元组了,后面在执行Math.atan2这段代码的时候,编译器就能精确的推断出参数解构后是两个值的参数了。
再看一个例子,
let m = 100;
let n="abc";
let array = [m,n] ;
let f = array[0];
f = 2000;
f="aaa";
f可以被赋值为number和string任意类型都不会报错,但是其实我们想要的是f对应的是m的类型,也就是number类型,当他赋值成非number类型的时候应当会报错。 先解释下为啥不会报错,这是因为虽然m是number类型n是string类型,但是加到数组里后, array的类型就变成了更为宽泛的 (number|string)[]了,所以后面数组元素重新赋值只要是string或者number类型的都会通过。
let m = 100;
let n="abc";
let array = [m,n] as const;
let f = array[0];
f = 2000;
f="aaa";
使用as const后就可以解决上述问题,因为会将array类型变成readonly的元数组类型,而元数组类型是能记录数组每个位置的类型的。
回到最开始的代码,要想使返回的ts类型不是更为宽泛的数组类型而是我们想要的具体类型,需要使用到as const。
function zip(target: unknown[], source: unknown[]): unknown[]
function zip<Target extends unknown[], Source extends unknown[]>(target: Target, source: Source): Zip<Target, 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)]
}
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>]
: []
: []
;
const res = zip([1,2,3] as const, [5,6,7] as const)
这时候又出现新的问题了,由于as const会将数组变成只读的数组类型,这样zip函数声明定义的参数类型就不匹配了。 修改代码如下,
function zip(target: unknown[], source: unknown[]): unknown[]
function zip<Target extends readonly unknown[], Source extends readonly unknown[]>(target: Target, source: Source): Zip<Target, 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)]
}
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>]
: []
: []
;
const res = zip([1,2,3] as const, [5,6,7] as const)
但这样 Zip 函数的类型又不匹配了。 难道要把所有用到这个类型的地方都加上 readonly 么? 不用,我们 readonly 的修饰去掉就行了。
step4. 实现去除 readonly 的高级类型
Typescript 有内置的高级类型 Readonly:
type Readonly<T> = { readonly [P in keyof T]: T[p]; }
可以把索引类型的每个索引都加上 readonly 修饰:
但没有提供去掉 Readonly 修饰的高级类型,我们可以自己实现一下:
type Mutable<Obj> = {
-readonly [k in keyof Obj]: Obj[k]
}
step5. 最终
最终代码如下:
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)]
}
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 [k in keyof Obj]: Obj[k]
}
const res = zip([1,2,3] as const, [5,6,7] as const)