Open xxleyi opened 4 years ago
// 母类型
interface Action {
type: string
}
// 兼容字符串的子类型
type ActionOrStr<A extends Action> = string | A;
// 提取子类型的 fields name,结果是个 union 类型,并去除母类型 Action 中的字段
type ActionKey<T extends Action> = keyof Omit<T, keyof Action>
// 子类型 fields name 构成的数组类型
type ActionKeys<T extends Action> = ActionKey<T>[]
// 与子类型 fields name 构成的数组类型对应的 fileds value 构成的 tuple 类型,可以参数顺序一一对应
type ActionValues<T extends Action, Ks extends ActionKeys<T>> = {[K in keyof Ks]: Ks[K] extends ActionKey<T> ? T[Ks[K]] : never}
// 返回子类型字符串
function t<A extends Action>(type: string): ActionOrStr<A> {
return type;
}
// 返回子类型构造函数
function createAction<A extends Action, K extends ActionKeys<A>, V extends ActionValues<A, K> >(
type: ActionOrStr<A>,
...fields: K
): (...values: V) => A {
if (fields.length !== new Set(fields).size) throw Error("duplicated property names")
return (...values: V): A => {
return {type, ...Object.fromEntries(zip(fields, values))} as A
}
}
// 子类型之一
interface SomeAction extends Action {
id: number;
name: string;
}
// 相应的子类型构造函数
const someAction = createAction(t<SomeAction>('someAction'), 'id', 'name');
// 构造一个子类型实例
console.log(someAction(1, 'Tom'));
// this should emit error: Argument of type 'string' is not assignable to parameter of type 'number'.
// 构造函数有能力确保参数类型安全
console.log(someAction('Tom', 1));
// 辅助 zip 函数,典型的范型用法
function zip<K, V>(a: K[], b: V[]): (K | V)[][] {
// a 比 b 长的话,截断
if (a.length > b.length) a.length = b.length
return a.map((v, i) => [v, b[i]])
}
infer 可用于提取类型里面的类型参数,有诸多妙用:
// ReturnType 已经是内置的类型推导工具
type ReturnType2<T> = T extends (...args: any[]) => infer R ? R : T
type Test = ReturnType2<() => [number, string]>
infer R
出现在 extends
后面的判断条件中,占据着函数类型的返回值类型,在 T
确实是函数类型时,便返回这个 R
,否则返回 T
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
type Test = UnionToIntersection<{a: string, b: string} | {a: string, c: string}>
jcalz 大神在一个 stackoverflow 上给出的 demo: Typescript check object by type or interface at runtime with typeguards in 2020+ - Stack Overflow
namespace G {
export type Guard<T> = (x: any) => x is T;
export type Guarded<T extends Guard<any>> = T extends Guard<infer V> ? V : never;
const primitiveGuard = <T>(typeOf: string) => (x: any): x is T => typeof x === typeOf;
export const gString = primitiveGuard<string>("string");
export const gNumber = primitiveGuard<number>("number");
export const gBigint = primitiveGuard<bigint>("bigint");
export const gBoolean = primitiveGuard<boolean>("boolean");
export const gSymbol = primitiveGuard<symbol>("symbol");
export const gNull = (x: any): x is null => x === null;
export const gUndefined = (x: any): x is undefined => x === undefined;
export const gObject =
<T extends object>(propGuardObj: { [K in keyof T]: Guard<T[K]> }) =>
(x: any): x is T => typeof x === "object" && x !== null &&
(Object.keys(propGuardObj) as Array<keyof T>).
every(k => (k in x) && propGuardObj[k](x[k]));
export const gArray =
<T>(elemGuard: Guard<T>) => (x: any): x is Array<T> => Array.isArray(x) &&
x.every(el => elemGuard(el));
export const gUnion = <T, U>(tGuard: Guard<T>, uGuard: Guard<U>) =>
(x: any): x is T | U => tGuard(x) || uGuard(x);
}
const _gWriter = G.gObject({
name: G.gString,
age: G.gNumber,
});
interface Writer extends G.Guarded<typeof _gWriter> { }
const gWriter: G.Guard<Writer> = _gWriter;
const _gBook = G.gObject({
id: G.gNumber,
name: G.gString,
tags: G.gUnion(G.gArray(G.gString), G.gNull),
writers: G.gArray(gWriter)
})
interface Book extends G.Guarded<typeof _gBook> { }
const gBook: G.Guard<Book> = _gBook;
// ================= used and defined in components =================
function isBook(obj: any): obj is Book {
if (!gBook(obj)) return false //checking for shape and simple types
// cheking for specific values and ranges
if (obj.id < 1) return false
if (obj.writers && obj.writers.some(({ age }) => age < 5 || age > 150)) return false
return true
}
const book = JSON.parse('{"id":1,"name":"Avangers","tags":["marvel","fun"],' +
'"writers":[{"name":"Max","age":25},{"name":"Max","age":25}]}');
if (gBook(book)) {
console.log(book.name.toUpperCase() + "!"); // AVANGERS!
}
console.log(isBook(book)) // true or false
console.log(gBook(null))
const _gPerson = G.gObject({
name: G.gString
})
interface Person extends G.Guarded<typeof _gPerson> {}
const isStringArray = G.gArray<string>(G.gString)
const isNumberArray = G.gArray<number>(G.gNumber)
const isPerson = _gPerson
const isPersonArray = G.gArray<Person>(isPerson)
console.log(isPersonArray([{name: "XiXi"}, {name: "HuaHua", good: true}]))
const isStringOrPerson = G.gUnion<string, Person>(G.gString, isPerson)
console.log(isStringOrPerson(222))
console.log(isStringOrPerson("222"))
console.log(isStringOrPerson({name: "Xi"}))
typescript - How to exclude {} from another type? - Stack Overflow
type Z = {a: number} | {} | {b: boolean} | {c: string};
jcalz's answer is awesome, and after I understand it, I think maybe there is a way based on jcalz's, which can exclude the very same type.
type ExcludeExact<U, T> = U extends any ? T extends U ? (U extends T ? never : U) : U : never;
This works as you want:
type Y = ExcludeExact<Z, {}> // type Y = { a: number } | { b: boolean } | { c: string }
and works also in this way:
type Y = ExcludeExact<Z, {a: number}> // type Y = { } | { b: boolean } | { c: string }
but have a limit: only map to tuple and array types when we instantiate a generic homomorphic mapped type for a tuple or array
// array and tuple is also object, and can be mapped with keyof
// but have a limit: only map to tuple and array types
// when we instantiate a generic homomorphic mapped type for a tuple or array
interface PPP {
1: number
2: string
4: boolean
}
type Map1<T extends any[]> = {
[K in keyof T]: T[K] extends keyof PPP ? PPP[T[K]] : T[K]
}
// Map1 and Map2 can not produce same type shape, because until v4.0.2, TS still
// only map to tuple and array types when we instantiate a generic homomorphic mapped type for a tuple or array
// https://github.com/microsoft/TypeScript/issues/27995#issuecomment-433056847
type Map2<T extends any[], U extends keyof T> = {
[K in U]: T[K] extends keyof PPP ? PPP[T[K]] : T[K]
}
// workaround 1
type Map3<T extends any[], U extends keyof T> = {
[K in Exclude<U, keyof any[]>]: T[K] extends keyof PPP ? PPP[T[K]] : T[K]
} & { length: T['length'] } & any[]
// workaround 2
type Map4<T extends any[]> = {
[K in Exclude<keyof T, keyof any[]>]: T[K] extends keyof PPP ? PPP[T[K]] : T[K]
} & { length: T['length'] } & any[]
// check behavior
// Map1
type Y1 = Map1<[1, 2]> // type Y1 = [number, string]
type Y1IsObject = Y1 extends object ? true : false // true
type Y1IsArray = Y1 extends any[] ? true : false // true
type Y1IsTuple = Y1 extends [number, string] ? true : false // true
// Map2
type Y2 = Map2<[1, 2], keyof [1, 2]> // shape of type Y2 is not same as Y1
type Y2IsY1 = Y2 extends Y1 ? true : false // false
// Map3
type Y3 = Map3<[1, 2], keyof [1, 2]> // shape of type Y3 is same as Y1
type Y3IsY1 = Y3 extends Y1 ? true : false // true
// Map4
type Y4 = Map4<[1, 2]> // shape of type Y3 is same as Y1
type Y4IsY1 = Y4 extends Y1 ? true : false // true
// Y1
let x1: Y1 = [1, '2']
x1.length
x1[0] = 100
// Y2
let x2: Y2 = [1, '2'] // emit type error: [1, '2'] is not assignable to type Y2
// Y3
let x3: Y3 = [1, '2']
x3.length
x3[0] = 100
// Y4
let x4: Y4 = [1, '2']
x4.length
x4[0] = 100
// 面试题:类型转换
interface Action<T> {
payload?: T;
type: string;
}
interface Module {
count: number;
message: string;
asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>;
syncMethod<T, U>(action: Action<T>): Action<U>;
}
// 需求:在经过 Connect 函数之后,返回值类型为
interface Target {
asyncMethod<T, U>(input: T): Action<U>;
syncMethod<T, U>(action: T): Action<U>;
}
// 解:
// 提取对象中的函数名的 union
type FuncName<T> = {
[P in keyof T]: T[P] extends Function ? P : never
}[keyof T];
// 挑出对象的函数部分,构成新对象
type PickFunc<T> = Pick<T, FuncName<T>>
// 重整函数签名,需要使用条件类型 + infer
// {
// asyncMethod: <T, U>(input: Promise<T>) => Promise<Action<U>>;
// syncMethod: <T, U>(action: Action<T>) => Action<U>;
// }
// =>
// {
// asyncMethod<T, U>(input: T): Action<U>;
// syncMethod<T, U>(action: T): Action<U>;
// }
type ReshapeFunc<T> = {
[K in keyof T] : T[K] extends <I, R>(input: Promise<I>) => Promise<Action<R>>
? <I, R>(input: I) => Action<R>
: T[K] extends <A, R>(action: Action<A>) => Action<R>
? <A, R>(action: A) => Action<R>
: never
}
// 声明 Connect 函数类型
type Connect = (module: Module) => ReshapeFunc<PickFunc<Module>>
// 提取返回值类型
type Result = Connect extends (module: Module) => infer R ? R : never
// 校验结果是否满足需求
type CheckSame<T, U> = T extends U ? (U extends T ? true : false) : false
type Success = CheckSame<Result, Target>
What is TypeScript?
TS 作为 JS 的超集,不提供任何新的运行时功能,所有的功能均在编译期间完成,编译产出是一份抹除了所有静态类型的 JS 代码。整个编译过程负责发现类型声明和断言,并以此进行类型推导,抛出发现的错误,最后抹除类型信息,输出 JS 代码。
因此,TS 的主要功能是提供更好的编码体验(如智能提示和安全的重构),以及为项目提供更好的可维护性。
TS 的底色是提供一个尽可能安全且聪明的类型推导编译器,然后在这底色之上,经过深思熟虑,又有意开了一些不合类型安全的推导规则:
Generic:
Basic Types
除了 JS 自身的基本类型:
TS 提供了更多的基本类型:
Tuple types allow you to express an array with a fixed number of elements whose types are known, but need not be the same. And of course, Tuple data are mutated.
An enum is a way of giving more friendly names to sets of numeric values.
We want to provide a type that tells the compiler and future readers that this variable could be anything. However, when we use Unknown data type, we should narrow it to something more specific by doing typeof checks, comparison checks, or more advanced type guards.
In some cases, we might want to opt-out of type checking. After all, remember that all the convenience of any comes at the cost of losing type safety. Type safety is one of the main motivations for using TypeScript and you should try to avoid using any when not necessary.
any
is compatible with any and all types in the type system. This means that anything can be assigned to it and it can be assigned to anything. This is demonstrated in the example below:If you are porting JavaScript code to TypeScript, you are going to be close friends with any in the beginning. However, don't take this friendship too seriously as it means that it is up to you to ensure the type safety. You are basically telling the compiler to not do any meaningful static analysis.
void is a little like the opposite of any: the absence of having any type at all. The typical use case is to signify that a function does not have a return type. Such as:
The never type represents the type of values that never occur:
虽然 TS 中也有 null 和 undefined,但使用上还是有根本差异:
How they are treated by the type system depends on the
strictNullChecks
compiler flag. When instrictNullCheck:false
, thenull
andundefined
JavaScript literals are effectively treated by the type system the same as something of typeany
. These literals can be assigned to any other type. This is demonstrated in the below example:strictNullChecks
最好为开启状态。以上就是 TS 所有的基本类型了,但是竟然不包括 Function ?这是怎么一回事?因为 Function 呢,在 JS 中是真真正正的 Object 中的一类,Object 下的这种成员还包括 Array, Set, Map 等。Function 在其中确实是比较特殊的一种,因为 typeof 具备直接识别 Function 的能力,但 Array 也不甘落后,因为同样有 Array.isArray 的静态方法可以判断一个 data 是不是 Array。
如此优待的原因仅仅是它们特别常用,Object, Function, Array 也需要单独另作详细介绍。
Unions and Intersection Types
TS 拥有的是一个结构化类型系统,除却基本的类型之外,还可以自由衍生各种更具体或者更复杂的类型,基本的 union 和 intersection 当然必不可少。
最简单且好用的 union:
稍稍复杂一些的 union:
再复杂一些的 union:
Discriminating Unions: 一个常见且有用的编码技术
A common technique for working with unions is to have a single field which uses literal types which you can use to let TypeScript narrow down the possible current type.
Intersection Types
Intersection types are closely related to union types, but they are used very differently. An intersection type combines multiple types into one. This allows you to add together existing types to get a single type that has all the features you need.
Intersection 可以让我们写一些小类型,然后自由组合为大类型,复用类型,减少冗余逻辑与代码。
Type Inference
任何新的静态类型语言,如果没有类型推导,那就必然没有前途。
类型推导建立在静态类型,数学理论与编程共识之上。
比如
let a = 88
的意图就是确切的,完全等价于let a: number = 88
比如let a = [1, '1']
的意图也是比较确切的,外加编程共识,完全等价于let a: (number | string)[] = [1, '1']
比如let a = () => 88
的意图就是确切的,完全等价于let a = (): number => 88
,也完全等价于let a: () => number = (): number => 88
由此可见,类型推导是一种能力,这种能力需要程序员的觉知与配合意识,彼此合作,建立类型契约,减少代码逻辑冗余,增强代码可读性。
此外,TS 还有上下文推导,指程序在上下文中应该具备的类型:
作为
window.onmousedown
的处理函数的参数,mouseEvent 在默认情况不应该包含buton
属性。Advanced Types: 与 Utility Types 相得益彰
strictNullChecks
flag, and optional parameters and properties?:
, and optional chaining?.
stringOrNull === null
is also a type guards only handle typenull
, becausetypeof null
isobject
in JSstringOrNull ?? "default"
can and only handlenull
andundefined
, which is a awesome thinglet a = null as number
user!.email!.length
Type aliases create a new name for a type. Type aliases are sometimes similar to interfaces, but can name primitives, unions, tuples, and any other types that you’d otherwise have to write by hand. Aliasing doesn’t actually create a new type - it creates a new name to refer to that type.
Just like interfaces, type aliases can also be generic - we can just add type parameters and use them on the right side of the alias declaration:
We can also have a type alias refer to itself in a property:
Together with intersection types, we can make some pretty mind-bending types:
As we mentioned, type aliases can act sort of like interfaces; however, there are some subtle differences.
Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs a interface which is always extendable.
Because an interface more closely maps how JavaScript object work by being open to extension, we recommend using an interface over a type alias when possible.
On the other hand, if you can’t express some shape with an interface and you need to use a union or tuple type, type aliases are usually the way to go.
As mentioned in our section on enums, enum members have types when every member is literal-initialized.
Much of the time when we talk about “singleton types”, we’re referring to both enum member types as well as numeric/string literal types, though many users will use “singleton types” and “literal types” interchangeably.
A polymorphic this type represents a type that is the subtype of the containing class or interface. This is called F-bounded polymorphism, a lot of people know it as the fluent API pattern.
keyof Car
=> "propertyNameA" | "propertyNameB"keyof
andT[K]
interact with index signatures. An index signature parameter type must be ‘string’ or ‘number’. If you have a type with a string index signature,keyof T
will bestring | number
(and not juststring
, since in JavaScript you can access an object property either by using strings (object["42"]
) or numbers (object[42]
)). If you have a type with a number index signature,keyof T
will just benumber
.TypeScript provides a way to create new types based on old types — mapped types. In a mapped type, the new type transforms each property in the old type in the same way. For example, you can make all properties optional or of a type
readonly
. Here are a couple of examples:Readonly, Partial and Pick are homomorphic whereas Record is not. One clue that Record is not homomorphic is that it doesn’t take an input type to copy properties from:
Non-homomorphic types are essentially creating new properties, so they can’t copy property modifiers from anywhere.
Note that
keyof any
represents the type of any value that can be used as an index to an object. In otherwords,keyof any
is currently equal tostring | number | symbol
.A conditional type selects one of two possible types based on a condition expressed as a type relationship test:
A conditional type
T extends U ? X : Y
is either resolved toX
orY
, or deferred because the condition depends on one or more type variables. WhenT
orU
contains type variables, whether to resolve toX
orY
, or to defer, is determined by whether or not the type system has enough information to conclude thatT
is always assignable toU
.Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation.
The distributive property of conditional types can conveniently be used to filter union types.
Conditional types are particularly useful when combined with mapped types.
Utility Types: 挺有用,值得掌握
Type Compatibility
A Note on Soundness
TypeScript’s type system allows certain operations that can’t be known at compile-time to be safe. When a type system has this property, it is said to not be “sound”. The places where TypeScript allows unsound behavior were carefully considered.
Unsoundness in TS means Type A is assignable to Type B in some cases.
The basic rule for TypeScript’s structural type system is that
x
is compatible withy
ify
has at least the same members asx
. For example:Only members of the target type (Named in this case) are considered when checking for compatibility. This comparison process proceeds recursively, exploring the type of each member and sub-member.
A basic example:
To check if x is assignable to y, we first look at the parameter list. Each parameter in x must have a corresponding parameter in y with a compatible type. Note that the names of the parameters are not considered, only their types. The reason for this assignment to be allowed is that ignoring extra function parameters is actually quite common in JavaScript.
Now let’s look at how return types are treated, using two functions that differ only by their return type:
The type system enforces that the source function’s return type be a subtype of the target type’s return type.
When comparing the types of function parameters, assignment succeeds if either the source parameter is assignable to the target parameter, or vice versa. This is unsound because a caller might end up being given a function that takes a more specialized type, but invokes the function with a less specialized type. In practice, this sort of error is rare, and allowing this enables many common JavaScript patterns. A brief example:
You can have TypeScript raise errors when this happens via the compiler flag
strictFunctionTypes
.When comparing functions for compatibility, optional and required parameters are interchangeable. Extra optional parameters of the source type are not an error, and optional parameters of the target type without corresponding parameters in the source type are not an error.
When a function has a rest parameter, it is treated as if it were an infinite series of optional parameters.
This is unsound from a type system perspective, but from a runtime point of view the idea of an optional parameter is generally not well-enforced since passing undefined in that position is equivalent for most functions.
The motivating example is the common pattern of a function that takes a callback and invokes it with some predictable (to the programmer) but unknown (to the type system) number of arguments:
When a function has overloads, each overload in the source type must be matched by a compatible signature on the target type. This ensures that the target function can be called in all the same situations as the source function.
In order for the compiler to pick the correct type check, it follows a similar process to the underlying JavaScript. It looks at the overload list and, proceeding with the first overload, attempts to call the function with the provided parameters. If it finds a match, it picks this overload as the correct overload. For this reason, it’s customary to order overloads from most specific to least specific.
Enums are compatible with numbers, and numbers are compatible with enums. Enum values from different enum types are considered incompatible.
Classes work similarly to object literal types and interfaces with one exception: they have both a static and an instance type. When comparing two objects of a class type, only members of the instance are compared. Static members and constructors do not affect compatibility.
Private and protected members in a class affect their compatibility. When an instance of a class is checked for compatibility, if the target type contains a private member, then the source type must also contain a private member that originated from the same class. Likewise, the same applies for an instance with a protected member. This allows a class to be assignment compatible with its super class, but not with classes from a different inheritance hierarchy which otherwise have the same shape.
Because TypeScript is a structural type system, type parameters only affect the resulting type when consumed as part of the type of a member. For example,
In the above, x and y are compatible because their structures do not use the type argument in a differentiating way. Changing this example by adding a member to Empty shows how this works:
In this way, a generic type that has its type arguments specified acts just like a non-generic type.
For generic types that do not have their type arguments specified, compatibility is checked by specifying any in place of all unspecified type arguments. The resulting types are then checked for compatibility, just as in the non-generic case.
For example,
So far, we’ve used “compatible”, which is not a term defined in the language spec. In TypeScript, there are two kinds of compatibility: subtype and assignment. These differ only in that assignment extends subtype compatibility with rules to allow assignment to and from any, and to and from enum with corresponding numeric values.
Different places in the language use one of the two compatibility mechanisms, depending on the situation. For practical purposes, type compatibility is dictated by assignment compatibility, even in the cases of the implements and extends clauses.