TypeScript$Type-Manipulation-Produce

90 阅读1分钟

TypeScript$Type-Manipulation-Produce

如何基于已有类型 / 值来生成类型呢?

1. The keyof type operator

可以通过 keyof 获取对象类型的 keys 作为类型。

type Point = { x: number; y: number };
type P = keyof Point; // 'x' | 'y'

index signature:

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // number
 
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // string | number: number 会自动装换成 string,所以 number 也行
type DatePropertyNames = keyof Date
 
type DateStringPropertyNames = DatePropertyNames & string
type DateSymbolPropertyNames = DatePropertyNames & symbol

2. typeof Type Operator

如果想通过值 value 来获取类型 type,可以使用 typeof

需要注意的是,typeof ClassA 获取的是 function 的类型——即函数作为对象的类型。

let s = "hello";
let n: typeof s; // string
function f() {
  return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;
    /* type P = {
    x: number;
    y: number;
} */

3. Indexed Access Type

对于对象类型,我们可以通过索引获取其属性的类型。语法是Type[index],其中 Type 是对象类型,index 是类型:可以是 "name" 这样的字面量类型,也可以是 numbernumber | string 这样的类型及其组合。

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // number
type I1 = Person["age" | "name"];
type I2 = Person[keyof Person];
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName];

需要了解的是:

  • 索引不存在的属性会报错
  • index 是类型,不能使用变量来进行索引
const key = "age";
type Age = Person[key];
/* Error. Type 'key' cannot be used as an index type.
'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'? */

type key = "age";
type Age = Person[key];

Indexed Access Type 实战

对于数组,indexed access type 可以通过 number 获取元素的类型。需要注意的是,如果数组元素类型不同,得到的是 |

const MyArray = [
  { name: "Alice", age: 15 },
  { name: "Bob", age: 23 },
  { name: "Eve", age: 38 },
];
 
type Person = typeof MyArray[number];
       /* type Person = {
    name: string;
    age: number;
} */
type Age = typeof MyArray[number]["age"];
     // type Age = number
// Or
type Age2 = Person["age"];

4. Conditional Types

条件类型指的是一个类型可能有多种情况,具体哪种情况需要根据输入的情况来确认。表现形式为:SomeType extends OtherType ? TrueType : FalseType;

上面的说明中,多种情况指的是 TrueTypeFalseType。具体的类型取决于输入 SomeType 是否 extends OtherType

条件类型主要用在泛型上。因为泛型可以是任何类型,那么 someType extends OtherType 的结果就会根据泛型的不同而不同(someType 是泛型)。

interface IdLabel {
  id: number /* some fields */;
}
interface NameLabel {
  name: string /* other fields */;
}
 
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}

上面使用函数重载的。下面使用泛型+条件类型:

type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}
let a = createLabel("typescript"); // NameLabel

let b = createLabel(2.8); // IdLabel
 
let c = createLabel(Math.random() ? "hello" : 42); // NameLabel | IdLabel

可以看到,使用函数重载,为了支持 number | string 的情况,我们需要描述多种情况。而使用泛型+条件类型后,我们只需要一种类型:条件类型。我们把各种类型的情况的逻辑移到了条件类型本身的逻辑中,从而使外面的逻辑更简单。

Conditional Type Constraints

通过对 extends OtherType 中的 OtherType 进行约束,我们可以获取约束里的内容:

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
  message: string;
}
interface Dog {
  bark(): void;
}
 
type EmailMessageContents = MessageOf<Email>; // string
 
type DogMessageContents = MessageOf<Dog>; // never

在上面的例子中,我们获取了 { message: unknown } 里的 message 的信息。

type Flatten<T> = T extends any[] ? T[number] : T;
 // Extracts out the element type.
type Str = Flatten<string[]>; // string
 
// Leaves the type alone.
type Num = Flatten<number>; // number

Inferring Within Conditional Types

从约束条件里提取类型是一种很常见的操作,TypeScript 提供了语法糖:通过 infer 我们可以在 true 分支里获取约束条件里的类型。

type Flatten<T> = T extends Array<infer Item> ? Item : T;
type Flatten<T> = T extends any[] ? T[number] : T;

实战

我们可以使用 infer 关键字编写一些有用的辅助类型别名。例如,对于简单的情况,我们可以从函数类型中提取返回类型:

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;
 
type Num = GetReturnType<() => number>; // number
 
type Str = GetReturnType<(x: string) => string>; // string
 
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // boolean[]

对于有多个调用签名的类型(如重载函数),会对最后一个签名版型推断:

declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
 
type T1 = ReturnType<typeof stringOrNum>; // string | number

Distributive Conditional Types

Distributive Conditional Types 分布式条件类型。当条件类型作用于泛型类型时,它们在给定联合类型时变得可分配。🤨。简单来说,默认情况下,联合类型是先单独分开,再进行联合。

type ToArray<Type> = Type extends any ? Type[] : never;
 
type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]

为避免这种行为,可以用方括号将 extends 关键字的每一侧括起来。(联合类型被当做不可分割)

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
 
// 'ArrOfStrOrNum' is no longer a union.
type ArrOfStrOrNum = ToArrayNonDist<string | number>; //  (string | number)[]

5. Mapped Types

使用 index signatures,我们可以定义一个对象的一些未知的属性对应的类型。

映射类型 Mapped types 也类似,只不过 index signatures 是直接定义未知属性,而 mapped types 是通过泛型来获取这些属性。就像是通过泛型的 keys 来设置新的 types。

// index signatures:
type OnlyBoolsAndHorses = {
  [key: string]: boolean | Horse;
};
 
const conforms: OnlyBoolsAndHorses = {
  del: true,
  rodney: false,
};
// mapped types:
type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};

type Features = {
  darkMode: () => void;
  newUserProfile: () => void;
};
 
type FeatureOptions = OptionsFlags<Features>;
           /* type FeatureOptions = {
    darkMode: boolean;
    newUserProfile: boolean;
} */

Mapping Modifiers

对于 readonly?,我们可以通过 -+ 来删除/添加这些 modifiers。(默认 +

// Removes 'readonly' attributes from a type's properties
type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};
 
type LockedAccount = {
  readonly id: string;
  readonly name: string;
};
 
type UnlockedAccount = CreateMutable<LockedAccount>;
           /* type UnlockedAccount = {
    id: string;
    name: string;
} */
// Removes 'optional' attributes from a type's properties
type Concrete<Type> = {
  [Property in keyof Type]-?: Type[Property];
};
 
type MaybeUser = {
  id: string;
  name?: string;
  age?: number;
};
 
type User = Concrete<MaybeUser>;
      /* type User = {
    id: string;
    name: string;
    age: number;
} */

Key Remapping via as

有时候我们想改变 keys,这时候可以使用 as

type MappedTypeWithNewProperties<Type> = {
    [Properties in keyof Type as NewKeyType]: Type[Properties]
}
type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
 
interface Person {
    name: string;
    age: number;
    location: string;
}
 
type LazyPerson = Getters<Person>;
         /* type LazyPerson = {
    getName: () => string;
    getAge: () => number;
    getLocation: () => string;
} */
// Remove the 'kind' property
type RemoveKindField<Type> = {
    [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
 
interface Circle {
    kind: "circle";
    radius: number;
}
 
type KindlessCircle = RemoveKindField<Circle>;
           /* type KindlessCircle = {
    radius: number;
} */
type EventConfig<Events extends { kind: string }> = {
    [E in Events as E["kind"]]: (event: E) => void;
}
 
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
 
type Config = EventConfig<SquareEvent | CircleEvent>
       /* type Config = {
    square: (event: SquareEvent) => void;
    circle: (event: CircleEvent) => void;
} */
type ExtractPII<Type> = {
  [Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
 
type DBFields = {
  id: { format: "incrementing" };
  name: { type: string; pii: true };
};
 
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
                 /* type ObjectsNeedingGDPRDeletion = {
    id: false;
    name: true;
} */

6. Template Literal Types

模板字符串类型 template literal types 和 JavaScript 的 模板字符类似,只使用在了类型上。

type World = "world";
type Greeting = `hello ${World}`; // "hello world"
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`; // 所有组合

String Unions in Types

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
});
 
// makeWatchedObject has added `on` to the anonymous Object
 
person.on("firstNameChanged", (newValue) => {
  console.log(`firstName was changed to ${newValue}!`);
});
type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
 
/// Create a "watched object" with an `on` method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});
 
person.on("firstNameChanged", () => {});

Inference with Template Literals

type PropEventSource<Type> = {
    on<Key extends string & keyof Type>
        (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void): void;
};
 
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
 
const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});
 
person.on("firstNameChanged", newName => {
                                // (parameter) newName: string
    console.log(`new name is ${newName.toUpperCase()}`);
});
 
person.on("ageChanged", newAge => {
                          // (parameter) newAge: number
    if (newAge < 0) {
        console.warn("warning! negative age");
    }
})

Intrinsic String Manipulation Types

  • Uppercase<StringType>
  • Lowercase<StringType>
  • Capitalize<StringType>
  • Uncapitalize<StringType>