这是我参与「第五届青训营 」伴学笔记创造活动的第 8 天
TypeScript 介绍
- TypeScript 是 JavaScript 的超集,供给了 JavaScript 的一切功用,并供给了可选的静态类型、Mixin、类、接口和泛型等特性。
- TypeScript 的目标是经过其类型系统帮助及早发现过错并进步 JavaScript 开发功率。
- 经过 TypeScript 编译器或 Babel 转码器转译为 JavaScript 代码,可运转在任何浏览器,任何操作系统。
- 任何现有的 JavaScript 程序都能够运转在 TypeScript 环境中,并只对其中的 TypeScript 代码进行编译。
- 在完整保留 JavaScript 运转时行为的基础上,经过引进静态类型界说来进步代码的可维护性,减少或许呈现的 bug。
- 永远不会改动 JavaScript 代码的运转时行为,例如数字除以零等于 Infinity。这意味着,假如将代码从 JavaScript 迁移到 TypeScript ,即便 TypeScript 认为代码有类型过错,也能够保证以相同的方法运转。
- 对 JavaScript 类型进行了扩展,增加了例如
any
、unknown
、never
、void
。 - 一旦 TypeScript 的编译器完成了查看代码的作业,它就会 擦除 类型以生成终究的“已编译”代码。这意味着一旦代码被编译,生成的一般 JS 代码便没有类型信息。这也意味着 TypeScript 绝不会依据它揣度的类型更改程序的 行为。最重要的是,尽管或许会在编译过程中看到类型过错,但类型系统自身与程序如何运转无关。
- 在较大型的项目中,能够在独自的文件 tsconfig.json 中声明 TypeScript 编译器的装备,并细化地调整其作业方法、严厉程度、以及将编译后的文件存储在何处。
泛型
泛型是一种捕获参数类型的办法,用来创立能够在多种类型上作业可重用的组件,而不是单个类型,这样用户就能够以自己的数据类型来运用组件。
function identity<T>(arg: T): T {
return arg;
}
这儿,咱们运用了一个类型变量 T
,它是一种特别的变量,只用于表明类型而不是值。T
帮助咱们捕获用户传入的类型(比方:number
),之后咱们就能够运用这个类型。咱们再次运用了 T
当做回来值类型,这样参数类型与回来值类型便是相同的了。
咱们能够用两种方法调用一个泛型函数:
- 第一种方法是将一切参数(包括类型参数)传递给函数。
let output = identity<string>("myString");
// let output: string
这儿,咱们显式地将 T
设置为 string
,运用了 <>
括起来,并作为函数调用的参数之一。
- 第二种方法是最常见的,运用类型参数揣度,编译器依据传入的参数类型主动为咱们设置
T
的类型。
let output2 = identity("myString");
// let output2: string
不必在尖括号(<>
)中显式传递类型,编译器只是依据值 myString
,即可将 T
设置为其类型。尽管类型参数揣度是坚持代码更短、更可读的有用工具,但当编译器无法揣度类型时,比方在一些杂乱的情况下,仍是需求像第一种方法那样显式传递类型参数。
泛型变量
泛型变量代表的是任意类型。例如咱们要在一个函数中,打印一个参数的长度。由于运用这个函数的人或许传入的是个数字,而数字是没有 length
特点的,所以会报错。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // error: Property 'length' does not exist on type 'Type'.
return arg;
}
但假如咱们操作的是 T
类型的数组,length
特点是存在的。
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}
泛型函数 loggingIdentity
接纳泛型参数 T
和类型是 T[]
的数组参数 arg
,并回来类型是 T[]
的数组。
泛型类型
- 能够运用不同的泛型参数名,只需在数量上和运用方法上能对应上就能够。
let myIdentity: <Input>(arg: Input) => Input = identity;
- 还能够运用带有调用签名的目标字面量类型来界说泛型函数。
let myIdentity2: { <Type>(arg: Type): Type } = identity;
- 能够把上面比方里的目标字面量拿出来做为一个泛型接口。
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
- 能够把泛型参数也当作整个接口的一个参数,就能清楚的知道运用的详细是哪个泛型类型,接口里的其它成员也能知道这个参数的类型了。
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
let myIdentity2: GenericIdentityFn<string> = identity;
现在接口上有了一个非泛型函数签名,它是泛型类型的一部分,而不是描述泛型函数。当咱们运用 GenericIdentityFn
时,还需求指定相应的类型参数(这儿:number
),从而有效地锁定了之后代码里运用的类型。了解何时将类型参数直接放在调用签名上和接口自身上,将有助于描述类型的哪些方面是归于泛型的。
除了泛型接口,咱们还能够创立泛型类。可是,无法创立泛型枚举和泛型命名空间。
泛型类
泛型类与泛型接口相似,在类称号后边的尖括号(<>
)中有一个泛型类型参数列表。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
与接口一样,将类型参数放在类自身能够保证类的一切成员都运用同一类型。咱们知道,类有两部分:静态部分和实例部分。泛型类指的是实例部分的类型,所以类的静态成员不能运用类的泛型类型。
泛型束缚
咱们有时候想操作某类型的一组值,而且咱们知道这组值具有什么样的特点。 在 loggingIdentity
比方中,咱们想拜访 arg
的 length
特点,可是编译器并不能证明每种类型都有 length
特点,所以就报错了。咱们期望只需该类型具有此成员,咱们就答应运用它。
咱们需求创立一个包括.length
特点的接口,运用这个接口和 extends
关键字来实现束缚:
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
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});
在泛型束缚中运用类型参数
能够声明一个类型参数,且它被另一个类型参数所束缚。比方,现在咱们想要用特点名从目标里获取这个特点,而且咱们想要保证这个特点存在于目标 obj
上,因此咱们需求在这两个类型之间运用束缚。
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
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"'.
在泛型里运用类类型
运用泛型创立工厂函数时,需求经过其结构函数引用类类型。
function create<Type>(c: { new (): Type }): Type {
return new c();
}
更高级的示例运用原型特点来揣度并束缚结构函数与类实例的联系,Mixins 规划运用了此方式。
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;
类型转化(断言)
在处理类型时,有时需求重写变量的类型,例如库供给了不正确的类型。强制转化便是重写类型的过程。
as
转化变量的一种简略办法是运用 as
关键字,这将直接更改给定变量的类型。
let x: unknown = 'hello';
console.log((x as string).length);
强制转化实际上不会改动变量内数据的类型,例如,以下代码没有按预期作业,由于变量 x
仍然是一个数字。
let x: unknown = 4;
console.log((x as string).length); // prints undefined since numbers don't have a length
TypeScript 仍然会尝试对类型转化进行类型查看,以避免看起来不正确的类型转化,例如,由于 TypeScript 知道在不转化数据的情况下将字符串转化为数字是没有意义的,因此下面将抛出类型过错:
console.log((4 as string).length); // Error: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
<>
运用 <>
的作业原理与运用 as
转化相同。
let x: unknown = 'hello';
console.log((<string>x).length);
这种类型的转化不适用于 TSX
,例如在处理 React 文件时。
强制转化
若要掩盖 TypeScript 在强制转化时或许引发的类型过错,先强制转化为 unknown
类型,然后再转化为目标类型。
let x = 1;
console.log(((x as unknown) as string).length); // x实际上不是一个字符串,因此它将回来undefined
console.log((<string><unknown>x2).length); // 同上
类型护卫
假如一个值是联合类型,咱们只能拜访此联合类型的一切类型里共有的成员。
interface IBird {
fly();
layEggs();
}
interface IFish {
swim();
layEggs();
}
class Bird implements IBird {
fly(){}
layEggs(){}
}
class Fish implements IFish {
swim(){}
layEggs(){}
}
function getSmallPet(): IFish | IBird {
return Math.random() > 0.5 ? new Fish() : new Bird();
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
这个比方里, Bird
具有一个 fly
成员。 咱们不能确认一个 Bird | Fish
类型的变量是否有 fly
办法。假如变量在运转时是 Fish
类型,那么调用 pet.fly()
就出错了。
联合类型适合于那些值能够为不同类型的情况,咱们只能拜访联合类型中共同拥有的成员。但当咱们想确切地了解是否为 Fish
时怎么办?JavaScript 里常用来区分两个或许值的办法是查看成员是否存在。
// 每一个成员拜访都会报错
if (pet.swim) {
pet.swim();
} else if (pet.fly) {
pet.fly();
}
为了让这段代码作业,咱们需求运用类型断言:
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
自界说的类型维护
类型护卫便是一些表达式,它们会在运转时查看以保证在某个效果域里的类型。能够看到,上面咱们不得不多次运用类型断言,而经过类型护卫机制,咱们一旦查看过类型,就能在之后的每个分支里清楚地知道 pet
的类型了。要界说一个类型护卫,咱们只需简略地界说一个函数,它的回来值是一个类型谓词:
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
类型谓词为 parameterName is Type
方式(例如 pet is Fish
), parameterName
必须是来自于当前函数签名里的一个参数名。每逢运用一些变量调用 isFish
时,TypeScript 会将变量缩减为那个详细的类型,只需这个类型与变量的原始类型是兼容的。
// 'swim' 和 'fly' 调用都没有问题了
if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}
TypeScript 不只知道在 if
分支里 pet
是 Fish
类型;它还清楚在 else
分支里,一定是 Bird
类型。
typeof 类型护卫
假如像下面这样利用类型断言来写:
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(" ") + value;
}
if (isString(padding)) {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
这儿界说了每个函数来判别类型是否是对应原始类型,这太麻烦了。幸运的是,现在咱们不必将 typeof x === "number"
抽象成一个函数,由于 TypeScript 能够主动将它识别为一个类型护卫,也便是说咱们能够直接在内联代码块里查看类型。
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof
类型护卫只要两种方式能被识别:typeof v === "typename"
和 typeof v !== "typename"
, typename
必须是 number
, string
, boolean
或 symbol
。可是,TypeScript 不会阻止你与其它字符串比较,只是不会把那些表达式识别为类型护卫。
instanceof 类型护卫
instanceof
类型护卫是经过结构函数来缩小类型的一种方法。instanceof
的右侧要求是一个结构函数,TypeScript 将按照次序缩小为:
- 假如类型不为
any
,将是函数的prototype
类型。 - 结构函数回来的联合类型。
interface Padder {
getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString() {
return this.value;
}
}
function getRandomPadder() {
return Math.random() > 0.5 ? new SpaceRepeatingPadder(4) : new StringPadder(" ");
}
// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
padder; // 类型缩小为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
padder; // 类型缩小为'StringPadder'
}
空值类型护卫
运用类型维护来去除联合类型中的 null
与 JavaScript 写法共同。
function f(sn: string | null): string {
if (sn == null) {
return "default";
}
else {
return sn;
}
}
也能够运用短路运算符:
function f2(sn: string | null): string {
return sn ?? "default";
}
在编译器无法消除 null
或 undefined
的情况下,能够运用类型断言运算符手动删除它们,语法是后缀添加 !
,这将从标识符的类型中删除 null
和 undefined
类型。
interface UserAccount {
id: number;
email?: string;
}
function getUser(id: string): UserAccount | undefined {
return { email: '' } as any;
}
const user = getUser("admin");
user.id; // Object is possibly 'undefined'.
if (user) {
user.email.length; // Object is possibly 'undefined'.
}
// 假如确认这些目标或字段存在,则添加短路可空性
user!.email!.length;