Type Manipulation类型操作
Generics 泛型 接收参数的类型
软件工程的一个主要部分是构建不仅具有定义良好且一致的api,而且还有可重用的组件。能够处理今天的数据以及明天的数据的组件将为构建大型软件系统提供最灵活的能力。
在c#和Java这样的语言中,创建可重用组件的工具箱中的主要工具之一是泛型,也就是说,能够创建可以在多种类型而不是单一类型上工作的组件。这允许用户使用这些组件并使用他们自己的类型。
泛型的Hello World
首先,让我们使用泛型的“hello world”:identity函数。恒等函数是: 一个函数它将返回传入的任何内容。
如果没有泛型,我们要么必须给恒等函数一个特定的类型:
function identity(arg: number):number {
return arg
}
或者,我们可以用any类型来描述恒等函数:
function identity(arg: any): any {
return arg;
}
虽然使用any肯定是通用的,因为它将导致函数接受any和all类型作为arg的类型,但我们实际上丢失了函数返回时该类型的信息。如果传入一个数字,那么可以返回任何类型。
相反,我们需要一种方法来捕获参数的类型,这样我们也可以用它来表示返回的内容。这里,我们将使用类型变量,这是一种特殊类型的变量,作用于类型而不是值。
function identity<Type>(arg:Type):Type {
return arg
}
我们现在在恒等函数中添加了类型变量type。这个类型允许我们捕获用户提供的类型(例如number),以便我们以后可以使用该信息。这里,我们再次使用Type作为返回类型。在检查时,我们现在可以看到实参和返回类型使用了相同的类型。这允许我们在函数的一端传输类型信息,并从另一端输出。
一旦我们写出了通用恒等函数,我们可以用两种方法之一来调用它。
第一种方法是将所有的实参,包括type实参,传递给函数:
let output = identity<string>('myString')
第二种可能也是最常见的。这里我们使用类型参数推断——也就是说,我们希望编译器根据传入的参数类型自动为我们设置type的值:
let output = identity('mySting')
注意,我们不需要在尖括号中显式传递类型(<>);编译器只是查看值“myString”,并将Type设置为其类型。虽然类型参数推断是一种有助于保持代码更短、更易读的工具,但当编译器无法推断类型时,您可能需要显式地传入类型参数,就像我们在前面的示例中所做的那样,这在更复杂的示例中可能会发生。
Working with Generic Type Variables(使用泛型类型变量)
当您开始使用泛型时,您会注意到,当您创建像identity这样的泛型函数时,编译器将强制您在函数体中正确地使用任何泛型类型的参数。也就是说,您实际上将这些参数视为可以是任意或所有类型。
如果我们还想在每次调用时将参数arg的length记录到控制台,该怎么办?我们可能会这样写:
function identity<Type>(arg:Type):Type {
// 上面的泛型参数可以是任意或所有类型 当参数为number时就是报错
// Property 'length' does not exist on type 'Type'.
console.log(arg.length)
return arg
}
假设我们实际上打算让这个函数处理Type数组,而不是直接处理Type,数组会有一个length的可用属性,我们可以像创建其他类型的数组一样来描述它:
function identity<Type>(arg:Type[]):Type[] {
console.log(arg.length)
return arg
}
我们也可以这样写示例:
function identity<Type>(arg:Array<Type>):Array<Type> {
console.log(arg.length)
return arg
}
Generic Types(泛型类型)
泛型函数的类型与非泛型函数一样,类型形参列在前面,类似于函数声明:
function identity<Type>(arg:Type):Type {
return arg
}
let myIndentity: <Type>(arg:Type) => Type = indentity
我们也可以在类型中为泛型类型参数使用不同的名称,只要类型变量的数量和类型变量的使用方式对齐即可。
function identity<Type>(arg:Type):Type {
return arg
}
let myIndentity: <Input>(arg:Input) => Input = indentity
我们也可以将泛型类型写成对象字面量类型的调用签名:
function identity<Type>(arg:Type):Type {
return arg
}
let myIdentity: {<Type>(arg:Type) => Type} = identity
使用接口抽离对象字面量类型
interface GenericIdentity {
<Type>(arg:Type): Type
}
function identity<Type>(arg:Type):Type {
return arg
}
let myIdentity:GenericIndentity = identity
在类似的示例中,我们可能希望将泛型参数移动为整个接口的参数。这让我们看到我们泛型的类型(例如:Dictionary而不仅仅是Dictionary)。这使得类型参数对接口的所有其他成员都可见。
interface GenericIdentityFn<Type> {
(arg:Type): Type
}
function identity<Type>(arg:Type):Type {
return arg
}
let myIdentity:GenericIdentityFn<number> = identity
请注意,我们的示例已经发生了些许变化。不再描述泛型函数,现在我们有一个非泛型函数签名,它是泛型类型的一部分。当我们使用GenericIdentityFn时,现在还需要指定相应的类型参数(这里是:number),从而有效地锁定底层调用签名将使用的内容。理解何时将类型参数直接放在调用签名上,何时将其放在接口本身上,将有助于描述类型的哪些方面是泛型。
Generic Classes(泛型类)
泛型类具有与泛型接口相似的形状。泛型类的泛型类型参数列表位于类名后面的尖括号(<>)中。
class GenericNumber<NumType> {
zeroValue: NumType
add: (x:NumType, y:NumType) => NumType
}
let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function(x, y) {
return x + y
}
这是GenericNumber类的字面用法,但您可能已经注意到,没有任何东西限制它只使用数字类型。我们可以使用字符串或更复杂的对象。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
与接口一样,将类型参数放在类本身上可以让我们确保类的所有属性都使用相同的类型。
Generic Constraints(泛型约束)
我们希望能够访问arg的.length属性,但编译器无法证明每一种类型都有.length属性,因此它警告我们不能做这个假设。这时我们可以使用泛型约束,只要类型有这个length成员,我们就允许它。
为此,我们将创建一个描述约束的接口。这里,我们将创建一个只有一个.length属性的接口,然后我们将使用这个接口和extends关键字来表示我们的约束:
interface LengthWise {
length: number
}
function loggingIdentity<T extends LengthWise>(arg:T):T {
console.log(arg.length)
return arg
}
因为泛型函数现在受到了约束,它将不再适用于任何和所有类型:
loggingIdentity(3);
Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
相反,我们需要传入其类型具有所有必需属性的值:
loggingIdentity({ length: 10, value: 3 });
Using Type Parameters in Generic Constraints(在泛型约束中使用类型参数)
可以声明受另一个类型参数约束的类型参数。例如,这里我们希望从给定名称的对象中获取属性。我们想要确保我们不会意外地抓取一个不存在于obj上的属性,所以我们将在这两种类型之间设置一个约束:
function getProperty<T, K extends keyOf T>(obj:T, key:K) {
return obj[key]
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
Using Class Types in Generics(泛型中使用类类型)
function create<T>(c: { new c(): T}):T {
return new c()
}
一个更高级的示例使用prototype属性来推断和约束构造函数和类类型的实例端之间的关系。
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
Keyof Type Operator(keyof类型操作符)
keyof操作符接受一个对象类型,并产生其键的字符串或数字字面值的并集。以下类型P与“x”|“y”类型相同:
type Point = { x: number; y: number };
type P = keyof Point;
// P的类型为keyof Point
如果类型有字符串或数字索引签名,keyof将返回这些类型:
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
// A的类型为number
type Mapish = { [k: string]: unknown};
type M = keyof Mapish;
// M的类型为string|number 原因是:JavaScript对象键总是被强制转换为字符串,所以obj[0]总是与obj["0"]相同。
type Mapish = { [k: string]: unknown; [k: symbol]: unknown }
type K = keyof Mapish
// K的类型为string|number|symble
Typeof Type Operator(typeof类型操作符)
TypeScript添加了一个typeof操作符,你可以在类型上下文中使用它来引用变量或属性的类型:
let s = "hello";
let n: typeof s;
// n是string类型
这对于基本类型不是很有用,但是与其他类型操作符结合使用,可以方便地使用typeof表示许多模式。举个例子,让我们从查看预定义的类型ReturnType开始。它接受一个函数类型并生成它的返回类型:
type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>;
// K是boolean类型 ReturnType是内置方法 它接受一个函数类型并生成它的返回类型
如果我们尝试在函数名上使用ReturnType,我们会看到一个指导性错误:
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<f>;
// 'f' refers to a value, but is being used as a type here. Did you mean 'typeof f'?
// “f”表示值,但在此处用作类型
记住,TypeScript有两大空间 类型空间 和值空间。 值和类型不是一回事。为了引用f值的类型,我们使用typeof:
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;
// 推导出P为:
type P = {
x: number;
y: number;
}
局限性: 具体来说,只有在标识符(即变量名)或其属性上使用typeof是合法的。
// Meant to use = ReturnType<typeof msgbox>
let shouldContinue: typeof msgbox("Are you sure you want to continue?");
',' expected.
Indexed Access Types(索引访问类型)
我们可以使用索引访问类型来查找另一类型上的特定属性:
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];
// Age是number类型
索引类型本身就是一个类型,所以我们可以使用union、keyof或其他类型:
type I1 = Person["age" | "name"];
// type I1 = string | number
type I2 = Person[keyof Person];
// type I2 = string | number | boolean
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName];
//type I3 = string | boolean
你会看到一个错误,如果你试图索引一个不存在的属性:
type I1 = Person["alve"];
// Property 'alve' does not exist on type 'Person'.
另一个使用任意类型进行索引的例子是使用number来获取数组元素的类型。我们可以将this和typeof结合起来方便地捕获数组字面量的元素类型:
const MyArray = [
{ name: 'Alice', age: 15, height: 180 },
{ name: 'Bob' },
{ name: 'Eve', age: 38 }
]
type Person = typeof MyArray[number];
// 得到Person的类型为:
type Person = {
name: string;
age: number;
height: number;
} | {
name: string;
age?: undefined;
height?: undefined;
} | {
name: string;
age: number;
height?: undefined;
}
// 使用
const p: Person = { name: 'Alice' }
type Age = typeof MyArray[number]['age']
// 得到Age类型为:
type Age = number | undefined
// 或者使用Person["age"]和上面typeof MyArray[number]['age']是一样的
type Age2 = Person["age"];
type Person0 = typeof MyArray[0]
你只能在索引时使用type,这意味着你不能使用const作为变量引用:
const key = "age";
type Age = Person[key];
// “key”表示值,但在此处用作类型。
// '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];
Conditional Types(条件类型)
在大多数有用程序的核心,我们必须根据输入做出决策。JavaScript程序也不例外,但考虑到值可以很容易地内省,这些决定也基于输入的类型。条件类型有助于描述输入和输出类型之间的关系。
条件类型的形式看起来有点像条件表达式(condition ?true表达式:false表达式)在JavaScript:
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
//type Example1 = number
type Example2 = RegExp extends Animal ? number : string;
//type Example2 = string
从上面的例子中,条件类型可能不会立即看起来有用, 接下来看下面的例子:
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";
}
createLabel的这些重载描述了一个基于其输入类型做出选择的单一JavaScript函数。注意几点:
- 如果一个库必须在其API中反复做出相同的选择,这将变得很麻烦。
- 我们必须创建三个重载:一个用于确定类型时的每一种情况(一个用于字符串,一个用于数字),一个用于最一般的情况(取字符串| number)。对于createLabel能够处理的每一个新类型,重载的数量都呈指数增长。
相反,我们可以将该逻辑编码为条件类型
type NameOrNumber<T extends string|number> = T extends number ? IdLabel : NameLabel
然后,我们可以使用条件类型将我们的重载简化为一个没有重载的函数。
function createLabel<T extends number|string>(idOrName:T):NameOrNumber<T> {
throw "unimplemented";
}
let a = createLabel("typescript");
// let a: NameLabel
let b = createLabel(2.8);
// let b: IdLabel
let c = createLabel(Math.random() ? "hello" : 42);
// let c: NameLabel | IdLabel
Conditional Type Constraints(条件类型约束)
通常,条件类型中的检查将为我们提供一些新信息。就像使用类型保护进行缩小可以提供更具体的类型一样,条件类型的真正分支将通过我们所检查的类型进一步约束泛型。
type MessageOf<T> = T["message"];
// 类型“"message"”无法用于索引类型“T”。
在这个例子中,TypeScript错误是因为T没有一个名为message的属性。我们可以约束T, TypeScript就不再错误了:
type MessageOf<T extends {message:unknow}> = T['message']
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>;
// EmailMessageContents是string类型
然而,如果我们希望MessageOf采用任何类型,并且在message属性不可用的情况下默认为never,该怎么办?我们可以通过移动约束并引入一个条件类型来做到这一点:
type MessageOf<T> = T extends {message:unknown} ? T['message'] : never
interface Email {
message:string
}
interface Dog {
bark(): void
}
type EmailMessageContents = MessageOf(Email)
// type EmailMessageContents = string
type DogMessageContents = MessageOf(Dog)
// type DogMessageContents = never
type Flatten<T> = T extends any[] ? T[number] : T;
// Extracts out the element type.
type Str = Flatten<string[]>;
type Str = string
// Leaves the type alone.
type Num = Flatten<number>;
type Num = number
Inferring Within Conditional Types(条件类型内的推理)
我们只是发现自己使用条件类型来应用约束,然后提取类型。这是一种非常常见的操作,条件类型使其变得更容易。
条件类型为我们提供了一种通过使用infer关键字从真实分支中比较的类型进行推断的方法。例如,我们可以在Flatten中推断元素类型,而不是使用索引访问类型“手动”获取元素:
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
这里,我们使用infer关键字声明性地引入了一个名为Item的新的泛型类型变量,而不是指定如何在真正的分支中检索T的元素类型。这使我们不必考虑如何挖掘和探查我们感兴趣的类型的结构。
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[]
type never = GetReturnType<123>;
// type never = never
当从具有多个调用签名的类型(例如重载函数的类型)进行推断时,将从最后一个签名进行推断(这可能是最允许的全捕获情况)。不能基于参数类型列表执行重载解析。
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>;
// type T1 = string | number
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: string | number): string | number;
declare function stringOrNum(x: number): string;
type T1 = ReturnType<typeof stringOrNum>;
// type T1 = string
Distributive Conditional Types(分配条件类型)
当条件类型作用于泛型类型时,当给定联合类型时,条件类型就成为分布类型。以以下内容为例:
type ToArray<Type> = Type extends any ? Type[] : never;
如果我们将一个联合类型插入到ToArray中,那么条件类型将被应用到该联合的每个成员。
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
// type StrArrOrNumArr = string[] | number[]
// ToArray<string | number>会分布触发
// ToArray<string> | ToArray<number>;
// string[] | number[];
通常,分散性是期望的行为。想要避免这种行为,可以用方括号[]将extends关键字的每一侧都括起来。
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'StrArrOrNumArr' is no longer a union.
type StrArrOrNumArr = ToArrayNonDist<string | number>;
// type StrArrOrNumArr = (string | number)[]
Mapped Types(映射类型)
当你不想重复自己的时候,有时候一种类型需要以另一种类型为基础
映射类型建立在索引签名的语法上,索引签名用于声明没有提前声明的属性类型:
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
horse: Horse
};
映射类型是一种泛型类型,它使用PropertyKeys的联合(通常通过keyof创建)来遍历键来创建类型: in是遍历联合类型的
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
在本例中,OptionsFlags将从type类型中获取所有属性,并将它们的值更改为布尔值
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<FeatureFlags>;
// 得到的类型如下
type FeatureOptions = {
darkMode: boolean;
newUserProfile: boolean;
}
Mapping Modifiers(映射修改)
在映射过程中有两个附加的修饰符:readonly和?分别影响可变性和可选性。
可以通过前缀-或+来删除或添加这些修饰符。如果不添加前缀,则使用+。
// 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>;
// 得到类型如下 去掉了readonly前缀
type UnlockedAccount = {
id: string;
name: string;
}
// Concrete类型为如果Type泛型变量的属性如果存在可选 则移除
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 viaas
(通过as重新映射键)
在TypeScript 4.1及以后的版本中,你可以使用映射类型中的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
}
注意: 上面的<string & Property>代表是property只能是string的属性,如果有别的(number,boolean,symble)的属性话 string & number|boolean|symble会得到一个never类型 结果就会过滤掉这个属性也相当于是跳过
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<
string & Property
>}`]: () => Type[Property]
}
interface Person {
name: string
age: number
location: string
1: number
'2': Object
// 接口中的计算属性名必须为 "string"、"number"、"symbol" 或 "any"
// [{a:1}]: boolean
}
type LazyPerson = Getters<Person>
// 得到的类型 属性为1被过滤掉了
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
get2: () => Object;
*}*
你可以通过条件类型生成never来过滤键:
// Remove the 'kind' property
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
// 内置类型Exclude
type Exclude<T, U> = T extends U ? never : T
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// 得到的类型
type KindlessCircle = {
radius: number;
}
你可以映射任意的并集,不仅仅是string | number | symbol的并集,还可以映射任何类型的并集:
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;
}
Further Exploration(进一步探究)
在这个类型操作部分,映射类型与其他特性配合得很好,例如,这里有一个使用条件类型的映射类型,根据对象的属性pii是否设置为真,返回true或false:
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;
}
注意:extends 在泛型中 T必须是U的子集 在条件类型中T extends U T继承自U类似于原型链继承T的方法可能多于U
Template Literal Type(模板文字类型)
模板文字类型建立在字符串文字类型的基础上,并且能够通过联合扩展成许多字符串。
它们的语法与JavaScript中的模板字面值字符串相同,但用于类型位置。当与具体文字类型一起使用时,模板文字通过连接内容生成一个新的字符串文字类型。
type World = "world";
type Greeting = `hello ${World}`;
// 得到的类型
type Greeting = "hello world"
当在插入位置使用联合时,该类型是每个联合成员所表示的每个可能的字符串字面值的集合:
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// 得到的类型
type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
对于模板字面量中的每个内插位置,联合相乘:
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
// 得到的类型 AllLocaleIDs 4个 Lang 3个 4*3=12个
type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id" | "en_footer_title_id" | "en_footer_sendoff_id" | "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" | "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id"
String Unions in Types(类型中的字符串组合)
模板字面值的强大之处在于基于类型中的现有字符串定义一个新字符串。
例如,JavaScript中的一个常见模式是基于对象当前拥有的字段扩展对象。我们将为函数提供一个类型定义,它增加了对on函数的支持,让你知道什么时候值发生了变化:
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
});
person.on("firstNameChanged", (newValue) => {
console.log(`firstName was changed to ${newValue}!`);
});
注意,在监听事件“firstNameChanged”时,而不仅仅是“firstName”,模板字面量提供了一种方法来在类型系统中处理这类字符串操作:
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
通过这个,我们可以构建一些在给定错误属性时出错的东西:
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
// 第一个参数必须是firstNameChanged|lastNameChanged|ageChanged之一
person.on("firstNameChanged", () => {});
// Prevent easy human error (using the key instead of the event name)
// firstName不是上面三个之一
person.on("firstName", () => {});
Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
// It's typo-resistant
// frstNameChanged也不是 少个i
person.on("frstNameChanged", () => {});
Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
Inference with Template Literals(使用模板文字进行推断)
注意,上一个示例没有重用原始值的类型。回调使用了any。模板文字类型可以从替换位置推断出来。
我们可以将最后一个示例设置为泛型,从eventName字符串的部分推断出关联的属性。
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");
}
})
这里我们把它变成了一个泛型方法。
当用户调用字符串“firstNameChanged”时,TypeScript会尝试推断Key的正确类型。为此,它将Key与“Changed”之前的内容进行匹配,并推断出字符串“firstName”。一旦TypeScript确定了这一点,on方法就可以获取原始对象上firstName的类型,在本例中是string。类似地,当调用“ageChanged”时,TypeScript会找到属性age的类型,即number。
推理可以以不同的方式组合,通常是解构字符串,并以不同的方式重建它们
Intrinsic String Manipulation Types(字符串内置操作类型)
为了帮助字符串操作,TypeScript包含了一组可用于字符串操作的类型。为了提高性能,这些类型是编译器内置的,在.d中找不到。TypeScript中包含的ts文件。
-
将字符串中的每个字符转换为大写版本。
Uppercase
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
// 得到类型
type ShoutyGreeting = "HELLO, WORLD"
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app">
// 得到类型
type MainID = "ID-MY_APP"
-
将字符串中的每个字符转换为等效的小写字母。
Lowercase
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>
// 得到类型
type QuietGreeting = "hello, world"
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">
// 得到类型
type MainID = "id-my_app"
-
将字符串中的第一个字符转换为等效的大写字母。
Capitalize
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
// 得到类型
type Greeting = "Hello, world"
-
将字符串中的第一个字符转换为等效的小写形式。
Uncapitalize
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
// 得到类型
type UncomfortableGreeting = "hELLO WORLD"