xxleyi / ts_note

learning note of static and flexible types and interfaces of TypeScript
0 stars 0 forks source link

1. types #1

Open xxleyi opened 4 years ago

xxleyi commented 4 years ago

What is TypeScript?

TS 作为 JS 的超集,不提供任何新的运行时功能,所有的功能均在编译期间完成,编译产出是一份抹除了所有静态类型的 JS 代码。整个编译过程负责发现类型声明和断言,并以此进行类型推导,抛出发现的错误,最后抹除类型信息,输出 JS 代码。

因此,TS 的主要功能是提供更好的编码体验(如智能提示和安全的重构),以及为项目提供更好的可维护性。

TS 的底色是提供一个尽可能安全且聪明的类型推导编译器,然后在这底色之上,经过深思熟虑,又有意开了一些不合类型安全的推导规则:

Generic:

Many algorithms and data structures in computer science do not depend on the actual type of the object. However, you still want to enforce a constraint between various variables.

Basic Types

除了 JS 自身的基本类型:

TS 提供了更多的基本类型:

  1. Tuple

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.

  1. Enum

An enum is a way of giving more friendly names to sets of numeric values.

  1. Unknown

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.

  1. Any

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:

var power: any;

// Takes any and all types
power = '123';
power = 123;

// Is compatible with all types
var num: number;
power = num;
num = power;

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.

  1. Void

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:

function warnUser(): void {
  console.log("This is my warning message");
}
  1. Never

The never type represents the type of values that never occur:

// Function returning never must not have a reachable end point
function error(message: string): never {
  throw new Error(message);
}

// Inferred return type is never
function fail() {
  return error("Something failed");
}

// Function returning never must not have a reachable end point
function infiniteLoop(): never {
  while (true) {}
}

虽然 TS 中也有 null 和 undefined,但使用上还是有根本差异:

How they are treated by the type system depends on the strictNullChecks compiler flag. When in strictNullCheck:false, the null and undefined JavaScript literals are effectively treated by the type system the same as something of type any. These literals can be assigned to any other type. This is demonstrated in the below example:

var num: number;
var str: string;

// These literals can be assigned to anything
num = null;
str = undefined;

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:

type LoadState = "loading" | "loaded"
let s: LoadState
// 变量 s 只能取两个值:"loading" 或 "loaded"

稍稍复杂一些的 union:

type StrOrNum = string | number
let a: StrOrNum
// 变量 s 只能取 string 或者 number 类型的值

再复杂一些的 union:

type Bird = {
  fly(): void;
  layEgg(): void;
}

type Fish = {
  swim(): void;
  layEgg(): void;
}

// 声明 getSmallPet 函数,但不用具体实现,此 demo 只用于类型推导和判断
declare function getSmallPet(): Bird | Fish;

// pet 变量被自动推导为 Bird | Fish 类型
let pet = getSmallPet();

// Bird | Fish 类型必然有 layEgg 方法,所以以下语句能够直接使用,没有 error
pet.layEgg();

// 但 Bird | Fish 类型不一定具有 swim 或者 fly 方法,所以以下两个语句会被 TS 编译器锁定为 error
pet.swim();
pet.fly();

// Bird | Fish 是很可能有 swim 或者 fly 方法的,只需要添加 type guards 即可正确通过编译
// 其实,这也是 JS 现实中真正的样子,只不过很可能是遇到过运行时 bug 之后才二次修改而来
// 对于优秀的程序员,理应一步到位
"swim" in pet && pet.swim()
"fly" in pet && pet.fly()

// Bird | Fish 是不可能有 yell 方法的,添加了 type guards 也无济于事,依然会被 TS 编译器锁定为 error
"yell" in pet && pet.yell()

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.

type NetworkLoadingState = {
  state: "loading";
};

type NetworkFailedState = {
  state: "failed";
  code: number;
};

// Create a type which represents only one of the above types
// but you aren't sure which it is yet.
type NetworkState =
  | NetworkLoadingState
  | NetworkFailedState;

function networkStatus(state: NetworkState): string {
  // Right now TypeScript does not know which of the two
  // potential types state could be.

  // Trying to access a property which isn't shared
  // across all types will raise an error
  state.code;

  // By switching on state, TypeScript can narrow the union
  // down in code flow analysis
  switch (state.state) {
    case "loading":
      return "Downloading...";
    case "failed":
      // The type must be NetworkFailedState here,
      // so accessing the `code` field is safe
      return `Error ${state.code} downloading`;
  }
}

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 = function(mouseEvent) {
    console.log(mouseEvent.buton);  //<- Error  
};

作为 window.onmousedown 的处理函数的参数,mouseEvent 在默认情况不应该包含 buton 属性。

Advanced Types: 与 Utility Types 相得益彰

  1. type guards
  1. type assertions: 程序员告诉编译器,相信我,不要在意,这正是我想要的,我知道自己在干什么
  1. Type Aliases: 赋予语义化更好的类型别名,增强类型系统表达能力,类似 c 语言的 typedef

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:

type Container<T> = { value: T };

We can also have a type alias refer to itself in a property:

type Tree<T> = {
  value: T;
  left?: Tree<T>;
  right?: Tree<T>;
};

Together with intersection types, we can make some pretty mind-bending types:

type LinkedList<Type> = Type & { next: LinkedList<Type> };

interface Person {
  name: string;
}

let people = getDriversLicenseQueue();
people.name;
people.next.name;
people.next.next.name;
people.next.next.next.name;
//                  ^ = (property) next: LinkedList
  1. Interfaces vs. Type Aliases

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.

interface A {
  a: string
}

interface B {
  b: string
}

// Type C can be re-opened without changing old code
interface C extends A {}
interface C extends B {}

let c : C = {a: '33', b: '44'}

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.

  1. Enum Member Types

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.

// from TS3.4+, we can apply a `const` assertion to a literal expression, which is also Enum
const HTTPRequestMethod = {
  CONNECT: "CONNECT",
  DELETE: "DELETE",
  GET: "GET",
  HEAD: "HEAD",
  OPTIONS: "OPTIONS",
  PATCH: "PATCH",
  POST: "POST",
  PUT: "PUT",
  TRACE: "TRACE"
} as const;

// 这等价于
const HTTPRequestMethod = {
  CONNECT: "CONNECT" as "CONNECT",
  DELETE: "DELETE" as "DELETE",
  GET: "GET" as "GET",
  HEAD: "HEAD" as "HEAD",
  OPTIONS: "OPTIONS" as "OPTIONS",
  PATCH: "PATCH" as "PATCH",
  POST: "POST" as "POST",
  PUT: "PUT" as "PUT",
  TRACE: "TRACE" as "TRACE"
};

// 是不是很方便?
  1. Polymorphic this types

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.

class F {
  run() {
    return this;
  }
}
  1. Index types: keyof Car => "propertyNameA" | "propertyNameB"

keyof and T[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 be string | number (and not just string, 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 be number.

  1. Mapped types

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:

type Partial<T> = {
  [P in keyof T]?: T[P];
};

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type PartialWithNewMember<T> = {
  [P in keyof T]?: T[P];
} & { newMember: boolean }

type Keys = "option1" | "option2";
type Flags = { [K in Keys]: boolean };

type NullablePerson = { [P in keyof Person]: Person[P] | null };

type PartialPerson = { [P in keyof Person]?: Person[P] };

type Nullable<T> = { [P in keyof T]: T[P] | null };
type Partial<T> = { [P in keyof T]?: T[P] };

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

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:

type ThreeStringProps = Record<"prop1" | "prop2" | "prop3", string>;

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 to string | number | symbol.

  1. Conditional Types

A conditional type selects one of two possible types based on a condition expressed as a type relationship test:

T extends U ? X : Y

A conditional type T extends U ? X : Y is either resolved to X or Y, or deferred because the condition depends on one or more type variables. When T or U contains type variables, whether to resolve to X or Y, or to defer, is determined by whether or not the type system has enough information to conclude that T is always assignable to U.

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.

  1. basic rule

The basic rule for TypeScript’s structural type system is that x is compatible with y if y has at least the same members as x. For example:

interface Named {
  name: string;
}

let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };
x = y;

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.

  1. what kinds of functions should be considered compatible

A basic example:

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

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:

let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });

x = y; // OK
y = x; // Error, because x() lacks a location property

The type system enforces that the source function’s return type be a subtype of the target type’s return type.

  1. Function Parameter Bivariance

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:

enum EventType {
  Mouse,
  Keyboard,
}

interface Event {
  timestamp: number;
}
interface MouseEvent extends Event {
  x: number;
  y: number;
}
interface KeyEvent extends Event {
  keyCode: number;
}

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
  /* ... */
}

// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + "," + e.y));

// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) =>
  console.log((e as MouseEvent).x + "," + (e as MouseEvent).y)
);
listenEvent(EventType.Mouse, ((e: MouseEvent) =>
  console.log(e.x + "," + e.y)) as (e: Event) => void);

// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));

You can have TypeScript raise errors when this happens via the compiler flag strictFunctionTypes.

  1. Optional Parameters and Rest Parameters

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:

function invokeLater(args: any[], callback: (...args: any[]) => void) {
  /* ... Invoke callback with 'args' ... */
}

// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));

// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));
  1. Functions with overloads

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.

  1. Enums

Enums are compatible with numbers, and numbers are compatible with enums. Enum values from different enum types are considered incompatible.

  1. Classes

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.

  1. Private and protected members in classes

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.

  1. Generics

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,

interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;

x = y; // OK, because y matches structure of x

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:

interface NotEmpty<T> {
  data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y; // Error, because x and y are not compatible

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,

let identity = function <T>(x: T): T {
  // ...
};

let reverse = function <U>(y: U): U {
  // ...
};

identity = reverse; // OK, because (x: any) => any matches (y: any) => any
  1. Subtype vs Assignment

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.

xxleyi commented 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]])
}
xxleyi commented 4 years ago

infer

infer 可用于提取类型里面的类型参数,有诸多妙用:

  1. 官方版例子,提取函数类型的返回值类型
// ReturnType 已经是内置的类型推导工具
type ReturnType2<T> = T extends (...args: any[]) => infer R ? R : T

type Test = ReturnType2<() => [number, string]>

infer R 出现在 extends 后面的判断条件中,占据着函数类型的返回值类型,在 T 确实是函数类型时,便返回这个 R,否则返回 T

  1. 将 union 转为 intersection: Transform union type to intersection type - Stack Overflow
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}>
xxleyi commented 4 years ago

通用 type guards

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"}))
xxleyi commented 4 years ago

剔除某个特定的类型,非父非子,就是它本身

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 }
xxleyi commented 4 years ago

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

Playground Demo

// 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
xxleyi commented 4 years ago

挑选函数,重整函数签名

playground demo

// 面试题:类型转换

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>