TIP
: 原文发表于:cirplan.github.io
前言
在上一篇文章,初步介绍了 TS
的内置工具类型,让我们对 TS
的类型转换有了初步的认识。接下来为了加深我们的理解,我们动手进行一系列的挑战。
这个挑战就是 type-challenges,antfu 大佬创建的。再次膜拜顶级大佬。
可以看到该挑战区分了 easy
、medium
、hard
、extreme
四大难度。点击进具体的题目,通过 Take a Challenge
进行作答,每一题会有对应的单元测试,测试通过代表你的解答是可以的。如果没有思路也没关系,可以通过 Check out Solutions
查看别人的思路。最后你有好的idea,可以 Share your Solutions
,和大家一起讨论分享。
准备
在开始挑战之前,你或许需要了解一些常见的类型操作符。
&
交叉类型,把多个类型合并为一个类型。
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
// type ColorfulCircle = {
// color: string;
// radius: number;
// }
|
联合类型,表示或的关系。
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
// Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.
Keyof
对于对象类型,根据其 keys
返回一个字符串或数字的字面量联合类型。
type Point = { x: number; y: number };
type P = keyof Point; // 'x' | 'y'
如果类型包含 字符串
或 数字
索引,keyof
则直接返回其类型。
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // type A = number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // type M = string | number
M
的结果是 string | number
是因为 js 的对象索引都是转换成字符串的。所以 obj[0]
等同于 obj["0"]
。
extends
有条件的类型,其语法:
T extends U ? X : Y
意思是若 T
能够赋值给 U
,那么类型是 X
,否则为 Y
。
让我们回顾下类型的分配规则。
type-assignment.png行代表赋值的类型,列表示被赋值的类型。就像 any
能够分配给 unknow
、object
、void
、undefined
、null
类型,但 never
不行。
对于 T extends U ? X : Y
,如果 T
的类型是 A | B | C
,则会被解析为 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
。
infer
在 extends
子语句中,可以使用 infer
进行类型推断,表示待推断的类型变量。这个变量可以在 true
分支中继续使用。
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;
type Num = GetReturnType<() => number>; // type Num = number
type Str = GetReturnType<(x: string) => string>; // type Str = string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // type Bools = boolean[]
好的,了解了以上操作符,也算入门了。快去尝试挑战吧。
常见问题
在我解答的过程中,有一些比较有意思的知识点,在这里分别说说。
[T] extends [X]
vs T extends X
遇到这个问题是在 296・Permutation,你可以先尝试解答。
T extends X
我们都知道,但为啥会有 [T] extends [X]
?下面来看两个例子。
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>; // type StrArrOrNumArr = string[] | number[]
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'StrArrOrNumArr' is no longer a union.
type StrArrOrNumArr = ToArrayNonDist<string | number>; // type StrArrOrNumArr = (string | number)[]
简单的说,当 T
是联合类型的时候,会对其每个类型进行分配。由 string | number
-> ToArray<string> | ToArray<number>
-> string[] | number[]
。
如果要避免这种行为,则可以通过添加 []
在 extends
关键字两侧来限制。更具体可以看文档解析。
接下来再看两个例子,能够很好的证明上面的观点。
type ToBooleanArr<T> = T extends boolean ? T[] : never;
type BooleanArr = ToBooleanArr<boolean> // type BooleanArr = false[] | true[]
type ToBooleanArr<T> = [T] extends [boolean] ? T[] : never;
type BooleanArr = ToBooleanArr<boolean> // type BooleanArr = boolean[]
看到这里你应该大概知道怎么回事了。接下来再看神奇的 never
判断。当我想要判断 T
是否是 never
:
type CheckNotNever<T> = T extends never ? false : true;
type CheckString = CheckNotNever<string> // type CheckString = true
type CheckNever = CheckNotNever<never> // type CheckNever = never
咦,发现没有,当传 never
的时候,CheckNever
预期应该是 false
,结果返回 never
。Why?
我们再看一个测试:
type UnionTypes = string | never | number // type UnionTypes = string | number
UnionTypes
是不包含 never
的。所以通过上面两点,我们可以知道在 T extends never
时,如果 T
是 never
,则表示空联合。那么当然不会执行后面的条件。
所以要判断 never
,我们只需要:
type CheckNotNever<T> = [T] extends [never] ? false : true;
type CheckString = CheckNotNever<string> // type CheckString = true
type CheckNever = CheckNotNever<never> // type CheckNever = false
对于上面这个问题,这里有个很好的解析,建议围观。
K extends K
这个判断会让人觉得很困惑,K
肯定是可以赋值给 K
的,那为啥还要判断?
其实上面也有解析,当 K
是联合类型的时候,会对每个类型进行分配,其结果会再联合。这其实可以理解为一个hack,以进行循环。
还是看这个解析,看完会有更全面的了解。
type Permutation<T, K=T> =
[T] extends [never]
? []
: K extends K
? [K, ...Permutation<Exclude<T, K>>]
: never
iterate.png
Object
vs object
vs {}
这三个类型是 TS
中让人比较困惑的存在,先看几个例子:
var o: object;
o = { prop: 0 }; // OK
o = []; // OK
o = 42; // Error
o = "string"; // Error
o = false; // Error
o = null; // Error
o = undefined; // Error
object
是 TS
2.2 版本新增的一种类型:表示非基础类型。即不是
number | string | boolean | symbol | null | undefined
的类型。
下面看 Object
和 {}
。这两个类型粗略一看一模一样啊,不是么?我们先看看 Object
的定义。
其中 Object
接口定义了 Object.prototype
原型对象上的属性。ObjectConstructor
接口定义了 Object
类的属性。
interface Object {
/** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */
constructor: Function;
/** Returns a string representation of an object. */
toString(): string;
/** Returns a date converted to a string using the current locale. */
toLocaleString(): string;
/** Returns the primitive value of the specified object. */
valueOf(): Object;
/**
* Determines whether an object has a property with the specified name.
* @param v A property name.
*/
hasOwnProperty(v: PropertyKey): boolean;
/**
* Determines whether an object exists in another object's prototype chain.
* @param v Another object whose prototype chain is to be checked.
*/
isPrototypeOf(v: Object): boolean;
/**
* Determines whether a specified property is enumerable.
* @param v A property name.
*/
propertyIsEnumerable(v: PropertyKey): boolean;
}
interface ObjectConstructor {
new(value?: any): Object;
(): any;
(value: any): any;
/** A reference to the prototype for a class of objects. */
readonly prototype: Object;
/**
* Returns the prototype of an object.
* @param o The object that references the prototype.
*/
getPrototypeOf(o: any): any;
/**
* Gets the own property descriptor of the specified object.
* An own property descriptor is one that is defined directly on the object and is not inherited from the object's prototype.
* @param o Object that contains the property.
* @param p Name of the property.
*/
getOwnPropertyDescriptor(o: any, p: PropertyKey): PropertyDescriptor | undefined;
/**
* Returns the names of the own properties of an object. The own properties of an object are those that are defined directly
* on that object, and are not inherited from the object's prototype. The properties of an object include both fields (objects) and functions.
* @param o Object that contains the own properties.
*/
getOwnPropertyNames(o: any): string[];
/**
* Creates an object that has the specified prototype or that has null prototype.
* @param o Object to use as a prototype. May be null.
*/
create(o: object | null): any;
/**
* Creates an object that has the specified prototype, and that optionally contains specified properties.
* @param o Object to use as a prototype. May be null
* @param properties JavaScript object that contains one or more property descriptors.
*/
create(o: object | null, properties: PropertyDescriptorMap & ThisType<any>): any;
/**
* Adds a property to an object, or modifies attributes of an existing property.
* @param o Object on which to add or modify the property. This can be a native JavaScript object (that is, a user-defined object or a built in object) or a DOM object.
* @param p The property name.
* @param attributes Descriptor for the property. It can be for a data property or an accessor property.
*/
defineProperty<T>(o: T, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): T;
/**
* Adds one or more properties to an object, and/or modifies attributes of existing properties.
* @param o Object on which to add or modify the properties. This can be a native JavaScript object or a DOM object.
* @param properties JavaScript object that contains one or more descriptor objects. Each descriptor object describes a data property or an accessor property.
*/
defineProperties<T>(o: T, properties: PropertyDescriptorMap & ThisType<any>): T;
/**
* Prevents the modification of attributes of existing properties, and prevents the addition of new properties.
* @param o Object on which to lock the attributes.
*/
seal<T>(o: T): T;
/**
* Prevents the modification of existing property attributes and values, and prevents the addition of new properties.
* @param a Object on which to lock the attributes.
*/
freeze<T>(a: T[]): readonly T[];
/**
* Prevents the modification of existing property attributes and values, and prevents the addition of new properties.
* @param f Object on which to lock the attributes.
*/
freeze<T extends Function>(f: T): T;
/**
* Prevents the modification of existing property attributes and values, and prevents the addition of new properties.
* @param o Object on which to lock the attributes.
*/
freeze<T extends {[idx: string]: U | null | undefined | object}, U extends string | bigint | number | boolean | symbol>(o: T): Readonly<T>;
/**
* Prevents the modification of existing property attributes and values, and prevents the addition of new properties.
* @param o Object on which to lock the attributes.
*/
freeze<T>(o: T): Readonly<T>;
/**
* Prevents the addition of new properties to an object.
* @param o Object to make non-extensible.
*/
preventExtensions<T>(o: T): T;
/**
* Returns true if existing property attributes cannot be modified in an object and new properties cannot be added to the object.
* @param o Object to test.
*/
isSealed(o: any): boolean;
/**
* Returns true if existing property attributes and values cannot be modified in an object, and new properties cannot be added to the object.
* @param o Object to test.
*/
isFrozen(o: any): boolean;
/**
* Returns a value that indicates whether new properties can be added to an object.
* @param o Object to test.
*/
isExtensible(o: any): boolean;
/**
* Returns the names of the enumerable string properties and methods of an object.
* @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
keys(o: object): string[];
}
/**
* Provides functionality common to all JavaScript objects.
*/
declare var Object: ObjectConstructor;
由于 js
的自动装箱特性,所以基础类型也是可以赋值给 Object
和 {}
。
var p: {}; // or Object
p = { prop: 0 }; // OK
p = []; // OK
p = 42; // OK
p = "string"; // OK
p = false; // OK
p = null; // Error
p = undefined; // Error
var q: { [key: string]: any };
q = { prop: 0 }; // OK
q = []; // OK
q = 42; // Error
q = "string"; // Error
q = false; // Error
q = null; // Error
q = undefined; // Error
var r: { [key: string]: string };
r = { prop: 'string' }; // OK
r = { prop: 0 }; // Error
r = []; // Error
r = 42; // Error
r = "string"; // Error
r = false; // Error
r = null; // Error
r = undefined; // Error
由于 Object
是有原型方法的,如果实现其方法,则会进行对应校验。
const obj1: Object = {
toString() { return 123 } // Error Type '() => number' is not assignable to type '() => string'. Type 'number' is not assignable to type 'string'.
};
但是 {}
则不一样。
const obj1: {} = {
toString() { return 123 } // Success
};
结论:Object
是包含了 toString
、hasOwnProperty
等方法的对象;{}
是空对象,但也能调用 Object
上的方法。可以说两者在运行时是没有差异的。但在编译时,{}
是不包含 Object
属性的,自然也不会进行校验。Object
则会进行对应校验,相比更加严格。
更加具体也可以查看 stackoverflow 上的这个回答。
最后
综上,希望能对你们了解 TS
类型系统有所帮助。同时,我把我的 type-challenges
解答整理在这里,有兴趣可以围观。
参考
- https://github.com/type-challenges/type-challenges
- https://www.typescriptlang.org/docs/handbook/intro.html
- https://github.com/type-challenges/type-challenges/issues/614
- https://stackoverflow.com/questions/49464634/difference-between-object-and-object-in-typescript
- http://www.semlinker.com/ts-object-type/
网友评论