在群里看到一些问题和言论:为什么你们这么喜欢“类型体操”?为什么我根本学不下去 TypeScript?我最讨厌那些做类型体操的了;为什么我学了没过多久马上又忘了?有感于这些问题,我想从最简单的一个角度来切入介绍一下 TypeScript,并向大家介绍并不是只要是个类型运算就是体操。并在文中介绍一种基本思想作为你使用类型系统的基本指引。
我将从一个相对简单的 API 的设计过程中阐述关于类型的故事。在这里我们可以假设我们现在是一个工具的开发者,然后我们需要设计一个 API 用于从对象中拿取指定的一些 key 作为一个新的对象返回给外面使用。
declare function pick(target: any, ...keys: any): any他的用户默默的写下了这段代码:
pick(undefined, 'a', 1).b
写完运行,发现问题大条了,控制台一堆报错,接口数据也提交不上去了,怎么办呢?
declare function pick(target: Record<string, unknown>, ...keys: string[]): unknown很好,上面的问题便不复存在了,API 也是基本可用的了。但是!当对象复杂的时候,以及字段并不是短单词长度的时候就会发现了一个没解决的问题。
pick({ abcdefghijkl: '123' }, 'abcdefghikjl')
从肉眼角度上,我们很难发现这前后的不一致,所以我们为什么要让调用方的用户自己去 check 自己的字段有没有写对呢?
declare function pick< T extends Record<string, unknown> >(target: T, ...keys: keyof T[]): unknown我们又进一步解决的上面的问题,但是!还是有着相似的问题,虽然我们不用检查 keys 是不是传入的是一个正确的值了,但是我们实际上对返回的值也存在一个类似的问题。
pick({ abcdefghijkl: '123' }, 'abcdefghijkl').abcdefghikjl一点小小的拓展
declare function pick< T extends Record<string, unknown>, Keys extends keyof T >(target: T, ...keys: Keys[]): { [K in Keys]: T[K] }到这里已经是对类型的作用有了基础的了解了,能写出来符合开发者所能接受的类型相对友好的代码了。我们可以再来思考一些更特殊的情况:
export type L2T<L, LAlias = L, LAlias2 = L> = [L] extends [never] ? [] : L extends infer LItem ? [LItem?, ...L2T<Exclude<LAlias2, LItem>, LAlias>] : never declare function pick< T extends Record<string, unknown>, Keys extends L2T<keyof T> >(target: T, ...keys: Keys): Pick<T, Keys[number] & keyof T> const x0 = pick({ a: '1', b: '2' }, 'a') console.log(x0.a) // @ts-expect-error console.log(x0.b) const x1 = pick({ a: '1', b: '2' }, 'a', 'a') // ^^^^^^^^ // TS2345: Argument of type '["a", "a"]' is not assignable to parameter of type '["a"?, "b"?] | ["b"?, "a"?]'. // Type '["a", "a"]' is not assignable to type '["a"?, "b"?]'. // Type at position 1 in source is not compatible with type at position 1 in target. // Type '"a"' is not assignable to type '"b"'.
一个相对来说比较完美的 pick 函数便完成了。