基础数据类型
JS的八种内置类型
let str: string = 'jimy';
let num: number = 24;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {x: 1};
let big: bigint = 100n;
let sym: symbol = Symbol('me');
tips:默认情况下,null和undefined是所有类型的子类型,可以将其赋值给其他类型。
如果在tsconfig.json中指定了“strictNullChecks”:true,null 和 undefined只能赋值给 void 和它们各自的类型。
number和bigint
虽然两者都表示数字,但是这两个类型不兼容。两者互相赋值,ts中会抛出类型不兼容的错误。
Array
数组定义有两种方式:
let arr: string[] = ['2', '3'];
let arr2: Array<string> = ['2', '3'];
定义联合类型数组:
let arr:(number | string)[]; // 该数组既可以存储数字,也可以存储字符串
定义指定对象成员的数组:
interface Arrobj {
name: string,
age: number
let arr: Arrobj[] = [{name: 'lemo', age: 23}]
函数声明:
function sum(x: number, y: number): number {
return x+y;
函数表达式
let mySum: (x: number, y: number) => number = function(x: number, y: number): number {
return x+y;
用接口定义函数类型:
采用函数表达式接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
interface SearchFunc{
(source: string, subString: string): boolean;
可选参数:
可选参数后面不允许再出现必需参数。
function buildName(firstName: string, lastName?: string) {
return lastName ? firstName + lastName : firstName;
参数默认值
function buildName(firstName: string, lastName: string = 'cat') {}
function buildName(firstName: string, ...otherName: any[]) {}
由于JS是一个动态语言,通常会使用不同类型参数来调用同一个函数,再根据参数不同返回不同的调用结果。当TS中开启了 noImplicitAny配置项时,则会提示参数具有隐式any类型。
function add(x: number, y: number): number;
function add(x: string, y: string): string;
type Types = number | string;
function add(x: Types, x: Types) {
if(typeof x === 'string') {
return x.toString() + y.toString()
return x + y;
Tuple(元祖)
数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这个情况就可以使用元祖。在JS中是没有元祖的,元祖是TS中特有的类型,其工作方式类似于数组。
元祖最重要的特征是可以限制 数组元素的个数和类型,特别适合用来实现多值返回。
元祖用于保存定长定数据类型的数据。
let x: [string, number] = ['hello', 123]
元祖类型的解构赋值
可以通过下标的方式来访问元祖中的元素,但是没有很便捷,元祖也是可以支持解构赋值的:
let employee: [number, string] = [1, 'str'];
let [id, username] = employee;
console.log(id, username); // 输出 1, str
元祖类型的可选元素
与函数签名类型,在定义元祖类型时,可以通过 ?来声明元祖类型的可选元素
let optionalTuple: [string, boolean?];
optionalTuple = ['xxx', true]; // 赋值时第二个元素可以不赋值
应用场景:在使用坐标时,坐标(x,y,z)可能是三维、二维、一维,定义坐标数据类型时,就可以选用可选参数进行定义。
元祖类型的剩余元素
元祖类型里最后一个元素可以是剩余元素,形式为...x,这里x是数组类型。剩余元素代表元祖类型是开放的,可以有零个或多个额外的元素。
type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "xxx", "yyy"]; // 除了接收第一个参数number类型,还可以接收多个string类型参数;
只读的元祖类型
const point: readonly [number, string] = [10, 'xxx']; // 定义一个只读的元祖
tips:加上readonly关键字前缀,任何企图修改元祖中元素的操作都会抛出异常。
void表示没有任何类型,和其它类型是平等关系,不能直接赋值:
let a: void;
let b: number = a; //Error
tip:只能为它赋予 null 和 undefined(在strictNullChecks 未指定为true时)。声明一个void类型的变量没有什么用处,一般用于函数无返回值时声明。
never
never类型表示的是那些永不存在的值的类型。
值会永不存在的两种情况:
1、如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值(因为抛出异常会直接中断程序运行,这使得程序运行不到返回值那一步,即具有不可达的终点,也就永不存在返回了。
2、函数中执行无限循环的代码(死循环),使得程序永远无法运行到函数返回值那一步,永不存在返回。
// 异常
function err(msg: string): never {
throw new Error(msg);
// 死循环
function loopForever(): never {
while(true) {};
tip:never 类型同 null 和 undefined 一样,也是任何类型的子类型,也可以赋值给任何类型。
但是没有类型是never的子类型或可以赋值给 never 类型(除了never本身之外),即使 any 也不可以赋值给never。
在TypeScript中,可以利用never类型的特性来实现全面检查,示例如下:
type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
if(typeof foo === 'string') {
// 这里foo 类型为 string类型
}else if(typeof foo === 'number') {
// 这里foo 类型为 number类型
}else {
// 这里foo 类型为 never
const check: never = foo;
tips:else分支里面,已经排除所有数据类型之后,将类型收窄为never的foo赋值给一个显示声明的never变量。想要收窄类型为never,必须先穷尽数据的所有可能类型。使用never避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。(在未穷尽类型时,赋值给never类型很可能会编译报错,穷尽了则可以正常赋值,本质上这块代码就相当于有了保障)。
在TypeScript中,任何类型都可以被归为any类型。这让any类型成为了类型系统的顶级类型。
普通类型在赋值过程中不允许被赋值其他类型的数据。而any类型赋值中可以被赋值为任意类型。any类型的数据上访问任何属性,调用任何方法都是被允许的。
变量如果声明的时候,未指定其类型,那么它会被识别为任意类型。
let something = 任意值;
// 等价于
let something: any = 任意值;
tips:为了开发规范,请尽量不要使用any。
unknown
为了解决any带来的问题,TypeScript3.0引入了 unknown类型。它与any一样,所有类型都可以分配给unknown。
unknown与any的最大区别是:任何类型的值都可以赋值给any
,同时any类型的值也可以赋值给任何类型。unknown
则任何类型的值都可以赋值给它,但它只能赋值给unknown和any。
function getDog() {
return '123'
const dog: unknown = {hello: getDog};
dog.hello(); // Error unknown不缩小类型的情况下,无法对unknown类型执行任何操作。
这种机制起到了很强的预防性,更安全,这就要求我们必须缩小类型,我们可以使用typeof、类型断言等方式来缩小未知范围。
function getDogName() {
let x: unknown;
return x;
const dogName = getDogName();
// 直接使用
const upName = dogName.toLowerCase(); // Error 未明确类型
// typeof
if(typeof dogName === 'string') {
const upName = dogName.toLowerCase(); // OK
// 类型断言
const upName = (dogName as string).toLowerCase(); // OK
Number、String、Boolean、Symbol
初学TS时,很容易和原始类型 number、string、boolean、symbol混淆的首字母大写的 Number、String、Boolean、Symbol类型,后者是相应原始类型的包装对象,即可以称之为对象类型。
从类型兼容性上看,原始类型兼容对应的对象类型,反过来对象类型不兼容对应的原始类型。
let num: number;
let Num: Number;
Num = num; // OK
num = Num; // ts(2322)报错
tips:铭记不要使用对象类型(即首字母大写)来注解值的类型,因为这没有任何意义。
object、Object 和 {}
object(首字母小写,称“小 object”)、Object(首字母大写,称“大Object”)和{}(称“空对象”)。
小object代表的是所有非原始类型,也就是说不能把number、string、boolean、symbol等原始类型赋值给object。在严格模式下,null和undefined类型也不能赋给object。
JavaScript中以下类型被视为原始类型:string、boolean、number、bigint、symbol、null 和 undefined。
大Object代表所有拥有 toString、hasOwnProperty方法的类型,所以所有原始类型、非原始类型都可以赋值给Object。同样,在严格模式下,null 和undefined类型也不能赋给Object。
总计:大Object包含原始类型,小object仅包含非原始类型,所以大Object似乎是小object的父类型。实际上,大Object不仅是小object的父类型,同时也是小object的子类型。
type isLowerCaseObjectExtendsUpperCaseObject = object extends Object ? true : false; // true
type isUpperCaseObjectExtendsLowerCaseObject = Object extends object ? true : false; // true
upperCaseObject = lowerCaseObject; // OK
lowerCaseObject = upperCaseObject; // OK
tips:尽管官方文档说可以使用小object代替大Object,但是我们仍要明白大Object并不完全等价于小object。
{} 空对象类型和大Object一样,也是表示原始类型和非原始类型的集合,并且在严格模式下,null 和 undefined 也不能赋给 {}。
综上所述:{}、大Object是比小object更宽泛的类型(least specific),{} 和 大Object可以互相代替,用来表示原始类型(null、undefined除外)和非原始类型;而小object则表示非原始类型。
定义基础类型的变量时都需要写明类型注解,这样在开发中很麻烦,特别是let定义变量时注解就算了,const定义变量也要写类型注解就有点麻烦了。
为了解决以上问题,TS在很多情况下,会根据上下文环境自动推断出变量的类型,无需我们再写明类型注解。
let str = 'this is string'; // 等价于 let str: string = 'this is string'
let num = 1; // 等价于 let num: number = 1
let bool = true; // 等价于 let bool: boolean = true
const str = 'this is string'; // 不等价于 const str: string = 'this is string'; 此时推断为字面量类型'this is string'
const num = 1; // 不等价于 const num: number = 1; 此时推断为字面量类型1
const bool = true; // 不等价于 const bool: boolean = true; 此时推断为字面量类型true
TypeScript这种基于赋值表达式推断类型的能力称之为 类型推断。
在TS中,具有初始化值的变量、有默认值的函数参数、函数返回的类型都可以根据上下文推断出来。
function add(a: number, b: number) { // 根据参数类型,推断出返回值类型也是number
return a + b;
const res1 = add(1, 3); // 推断出res1类型也是number
function add2(a: number, b = 1) { // b的类型是number或者undefined,返回类型也是number
return a + b;
const res2 = add2(1);
const res3 = add2(1, '2'); // 报错,参数2类型不是number | undefined
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成any类型而完全不被类型检查:
let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
有时我们会遇到这样的情况,我们比TS更了解某个值的详细信息,通常会发生在我们清楚知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式告诉编译器我们知道发生了什么操作。类型断言好比其他语言的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
const arrayNumber: number[] = [1,2,3,4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示ts(2322),因为此时返回有可能是undefined,类型undefined不能赋值给number类型
// 使用类型断言
const greaterThan2: nubmer = arrayNumber.find(num => num > 2) as number; //因为我们知道数组里肯定有大于2的数字,所以可以使用 as 借助类型断言确保TS按照我们的方式做类型检查(类似仅作用在类型层面的强制类型转换)
// 尖括号 语法
let someValue: any = "this is string";
let strLength: number = (<string>someValue).length;
// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
以上两种方式虽然没有任何区别,但是尖括号格式会与react中JSX语法冲突,因此更推荐使用as语法。
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 !可以用于断言操作对象是非null和非undefined类型。具体而言,x! 将从 x 值域中排除null和undefined。
let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); //OK 排除掉null 和 undefined,剩余一种类型为string,可以调用toSting方法
mayNullOrUndefinedOrString.toString(); //ts(2531) 变量类型可能为null | undefined,没办法调用toString
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
// Object is possibly 'undefined'.(2532) 对象可能是undefined
// Cannot invoke an object which is possibly 'undefined'.(2722) 不能调用当一个对象可能是undefined
const num1 = numGenerator(); //Error
const num2 = numGenerator!(); //OK
确定赋值断言
允许在实例属性和变量声明后放置一个 !号,从而告诉TS该属性会被明确地赋值。
let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454) 变量x在赋值前被使用
console.log(2 * x); //Error
// 使用确定赋值断言
let x!: number;
initailize();
console.log(2 * x); // 20
function initailize() {
x = 10;
通过 let x!: number; 确定赋值断言,TS编译器就会知道该属性会被明确地赋值。
字面量类型
在TS中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。
目前,TS支持3中字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型。
let specifiedStr: 'this is string' = 'this is string';
let specifiedNum: 1 = 1;
let specifiedBoolean: true = true;
使用字面量类型需注意:比如 'this is string'类型是string类型(确切地说是string类型的子类型),而string类型不一定是‘this is string'(这里表示一个字符串字面量类型)类型。
总结:'this is string'类型一定是 string类型,但是string类型不一定是 'this is string'类型,同样适用于数字、布尔等字面量和它们父类的关系。
let specifiedStr: 'this is string' = 'this is string';
let str: string = 'any string';
specifiedStr = str; // ts(2322) 类型string 不能赋值给类型 this is string
str = specifiedStr; //OK
使用示例:通过使用字面量类型组合的联合类型,更适用于我们限制函数的参数为指定的字面量类型集合,然后编译器会检查参数是否是指定的字面量类型集合里的成员。
let hello: 'hello' = 'hello';
hello = 'hi'; // ts(2322) 'hi'类型不能赋值给 类型'hello'
type Direction = 'up' | 'down';
function move(dir: Direction) {
//...
move('up'); // OK
move('right'); // ts(2345) 'right'类型参数不能赋值给 类型Direction
// 结合数字字面量类型及布尔字面量类型
// 定义接口Config,规定size属性为字符串字面量类型“small” | “big”,isEnable属性为布尔字面量类型true | false,margin属性为数字字面量类型 0 | 2 |4
interface Config {
size: 'small' | 'big';
isEnable: true | false;
margin: 0 | 2 | 4;
let 和 const 分析
const 定义变量时,在缺省类型注解的情况下,TS推断出它的类型直接由赋值字面量的类型决定。
let 定义变量时,缺省类型注解的情况下,TS推断类型为赋值字面量类型的父类型,如赋值为'this is string',推断为类型'this is string'父类型 string类型。
类型拓宽(Type Widening)
所有通过let或var定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显示添加类型注解的条件,那么它们推断出来的类型就是制定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。
const str = 'this is string'; // 类型是 'this is string' const定义的常量不会进行类型拓宽
let str2 = str; // 类型为拓宽后的类型 string,因为str没有显示显示注解类型
let Fun = (str = 'this is string') => str; // 类型是(str?: string) => string;
const str3: 'this is string' = 'this is string'; // 类型是 'this is string'
let str4 = str3; // 类型是 'this is string',因为str3显示注解了类型,即使使用let定义,类型也是str3显示注解的类型
除了字面量类型拓宽之外,TS对某些特定类型值也有类似“Type Widening”(类型拓宽)的设计:
比如对 null 和 undefined 的类型进行拓宽,通过let、var定义的变量如果满足未显示声明类型注解且被赋予了null 或 undefined值,则推断出这些变量的类型是any。
let x = null; // 类型拓宽成 any
let y = undefined; // 类型拓宽成 any
const z = null; // 类型是null
let anyFun = (param = null) => param; // 形参类型是null | undefined
let z2 = z; // 类型是null
let x2 = x; // 类型是null
let y2 = y; // 类型是undefined
tips:严格模式下,一些比较老的版本中(2.0)null 和 undefined不会被拓宽成any。
应用示例:假设在编写向量库,定义了Vector3接口,定义了getComponent函数用于获取指定坐标轴的值
interface Vector3 {
x: number;
y: number;
z: number;
function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
return vector[axis];
// 如果直接调用getComponent函数
let x = 'x'; // x类型拓宽为 string类型
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x); // Error x变量的string类型不能赋给类型 'x' | 'y' | 'z' 的参数
定义变量时,未显示注解类型,会出现很多可能性的推断
const arr = ['x', 1];
// 此时 arr 的类型可能是
('x' | 1)[];
['x', 1]
[string, number]
readonly [string,number]
(string | number)[]
readonly (string | number)[]
[any, any]
any[]
没有更多的上下文,TS无法知道哪种类型是正确的。
对此,TS提供了一些控制拓宽过程的方法。其中一种方法是使用 const。如果用const而不是let声明一个变量,那么它的类型会更窄。使用const可以帮助我们修复前面例子中的错误,如果使用 const x= 'x'; 则调用getComponent就可以正常执行。
注意:const对于对象和数组,仍然会存在问题。
const obj = { // TS解析中,obj的类型可以是{readonly x: 1}、{x: number}、{[key: string]: number} 或object类型
对于对象,TS的拓宽算法会将其内部属性视为其赋值给let关键字声明的变量,进而推断其属性的类型。
因此上面定义的 obj 的类型为 { x: number }。
有几种方法可以覆盖TS的默认行为,一种是提供显示类型注释:
const obj: {x: 1 | 3 | 5} = { // Type is { x: 1 | 3 | 5 }
另一种是使用const断言。切勿将其与 let 和 const混淆,后者在值空间引入符号。
const obj1 = { // Type is {x: number, y: number}
x: 1,
const obj2 = { // Type is {x: 1, y: number}
x: 1 as const,
const obj3 = { // Type is { readonly x: 1, readonly y: 2 }
x: 1,
} as const
当对一个值使用const断言时,TS将为它推断出最窄的类型,没有拓宽。对于真正的变量,这通常就是我们想要的,也可以对数组使用const断言。
const arr1 = [1,2,3]; // Type is number[]
const arr2 = [1,2,3] as const; // Type is readonly [1, 2, 3]
类型缩小(Type Narrowing)
既然有类型拓宽,就有类型缩小。在TS中,通过某些操作符将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是“Type Narrowing”。
比如,可以使用类型守卫将函数参数的类型从any缩小到明确的类型。
let func = (val: any) => {
if(typeof val === 'string') {
return val; // 类型是string
}else if(typeof val === 'number') {
return val; // 类型是number
return null;
还可以使用类型守卫将联合类型缩小到明确的子类型。
let func = (val: string | number) => {
if(typeof val === 'string') {
return val; // 类型是string
}else{
return val; // 类型是number
还可以通过字面量类型等值判断 (===)或其他控制流语句(包括但不限于 if、三目运算符、switch分支)将联合类型收敛为更具体的类型。
type Goods = 'pen' | 'pencil' | 'ruler';
const getPenConst = (item: 'pen') => 2;
const getPencilConst = (item: 'pencil') => 4;
const getRulerConst = (item: 'ruler') => 6;
const getConst = (item: Goods) => {
if(item === 'pen') {
return getPenConst(item); // item=> 'pen'
}else if(item === 'pencil') {
return getPencilConst(item); // item => 'pencil'
}else{
return getRulesConst(item); // item => 'rules'
以上代码解释:接受的参数类型是字面量类型的联合类型,函数内包含了if语句的三个分支流程,其中每个流程分支调用的函数的参数都是具体字面量类型。
由多个字面量组成的变量item可以传值给仅接收单一特定字面量类型的函数getPenConst等,是因为在每个流程分支中,编译器知道流程分支中item类型是什么。比如item === 'pen'的分支,item的类型就被收缩为 'pen'字面量类型。
如果去除pencil分支,则在else分支中推断出的item类型就是 'pencil' | 'rules'。
tips:在处理一些特殊值时要特别注意:它可能包含你不想要的东西。
例如:从联合类型中排除null 的方法是错误的:
const el = document.getElementById('foo'); // Type is HTMLElement | null
if(typeof el === 'object') { // null 也符合这个分支条件
el; // Type is HTMLElement | null;
通过取反判断参数:可选参数可能有undefined类型,空字符串和0都属于false值。
function foo(x?: number | string | null) {
if(!x) {
x; // Type is string | number | null | undefined;
帮助类型检查器缩小类型的另一种常见方法是在它们上放置一个明确的标签:
interface UploadEvent{
type: 'upload',
filename: string,
contents: string
interface DownloadEvent{
type: 'download',
filename: string
type AppEvent = UploadEvent | DownloadEvent;
function handleEvent(e: AppEvent) {
switch(e.type) {
case 'download':
e; // Type is DownloadEvent
break;
case 'upload':
e; // Type is UploadEvent
break;
这种模式也被称为“标签联合” 或 “可辨识联合”,在TS中应用范围非常广。
联合类型表示取值可以为多种类型中的一种, 使用 | 分隔每个类型。
联合类型通常与 null 或 undefined 一起使用,如函数参数 str: string | undefined,意味着该形参可以接收undefined。
类型别名用来给一个类型起个新名字。类型别名常用于联合类型。
type Message = string | string[]; // 给联合类型起了个别名
let greet = (message: Message) => {
// ...
类型别名,仅仅是给类型取了一个新的名字,并不是创建了一个新的类型。
交叉类型是将多个类型合并为一个类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特征,使用 & 定义交叉类型。
type Useless = string & number;
以上代码可见,将原始类型、字面量类型、函数类型等原子类型合并成交叉类型,没有任何用处,因为任何数据类型都不能满足同时属于多种原子类型,例如没有又是string又是number类型的数据。这个 Useless的类型就是never。
交叉类型的真正用处是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型。
type IntersectionType = { id: number, name: string } & { age: number };
const mixed: IntersectionType = {
id: 1,
name; 'name',
age: 18
上面代码,通过交叉类型,使得IntersectionType同时拥有了 id、name、age所有属性。(可以理解为两个接口类型的并集)
注意:合并可能出现同名属性时
当合并的接口类型中存在同名属性时:
属性类型不兼容时,合并后该属性就是多个属性类型的交叉类型,即都有name属性,一个为string,一个为number,合并后name属性就是 string & number,也就是never类型,此时对name做赋值操作会报错,且不定义name属性,则会提示缺少属性name。
属性类型兼容时,合并后该属性的类型就是该属性类型的交叉类型的子类型,如存在共同属性age,一个为number类型,一个为字面量类型2,则合并后age属性为字面量类型2,此时age只能赋值为2,赋予其他值则会报错。
当合并属性同名时,其值不是基本数据类型时,可以成功合并,如都有属性obj,各有属性 x、y,合并后obj属性则拥有x跟y两个属性。
接口(Interfaces)
在TS中,使用接口(Interface)来定义对象的类型。
什么是接口?
在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(class)去实现(implement)。
TS中的接口是一个非常灵活的概念,除了可用于【对类的一部分行为进行抽象】以外,也常用于对【对象的形状(Shape)】进行描述。
接口一般首字母大写,且定义的变量跟接口的属性要保持一致,即赋值的时候,变量的形状必须和接口的形状保持一致。
可选 | 只读属性
可选属性使用 ?标识符,如 name?: string, 则标识name属性为可选的string类型的属性。证明name属性可以不定义。
只读属性用于限制只能在对象刚刚创建的时候修改其值。此外TS还提供了ReadonlyArray<T>类型,它与Array<T>相似,只是把所有的可变方法去掉了,因此可以确保数组创建后不能再修改。
有时我们希望一个接口中包含必选和可选属性外,还可以支持其他属性,这时就可以使用 索引签名 的形式来实现。
interface Person {
name: string,
age?: number,
[propName: string]: any;
let tom: Person = {
name: 'Tom',
gender: 'male'
需要注意的是:一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集。如果任意属性的类型不包含其他定义属性的类型,则会报错,并且会将变量类型推断成了任意属性为联合类型。
一般任意属性需要明确类型,可以通过联合类型的形式,包含其他定义属性类型除外,还可以增加额外的类型。
interface Person {
name: string,
age?: number, // age类型为number | undefined
[propName: string]: string | number | undefined
鸭式辨型法
所谓的鸭式辨型法就是只要具有鸭子特征的就认为它就是鸭子,也就是通过制定规定来判定对象是否实现这个接口。
interface LabeledValue {
label: string
function printLabel(labelObj: labeledValue) {
console.log(labelObj.label);
// 借助鸭式辨型法
// 定义myObj变量,此时myObj不会经过额外属性检查,但会根据类型推断为 { size: number, label: string },
// 再给函数传递数据,给函数形参赋值,此时将myObj赋值给labelObj会进行类型兼容判断,因为都具有label属性,所以被认定为两个相同,所以可以用这个方法来避开多余的类型检查。
let myObj = { size: 10, label: 'Size 10 Object' };
printLabel(myObj); // OK
// 直接赋值操作
printLabel({size: 10, label: 'Size 10 Object'}); // Error 形参labelObj的类型不匹配,直接传值,相当于给函数形参赋值
总结:直接对变量赋值,会进行类型检查,而通过变量给变量赋值的形式,则是会进行类型兼容判断,而鸭式辨型法的判断,只要具有相同的属性,就会被认为两个类型相同,可以进行赋值操作,避开多余的类型检查。(直接赋值,进行类型检查,变量给变量赋值,进行类型兼容判断)
绕开额外属性检查的方式
1、鸭式辨型法(上面)
2、类型断言
类型断言的意义等同于告诉程序,我们清楚自己在做什么,此时程序自然就不会进行额外的属性检查了。通过as 操作符,将值当成某个类型进行对待。
interface Props{
name: string,
age?: number
let myProps: Props = {
name: 'xxx',
age: 18,
gender: 'male'
} as Props; // OK
3、索引签名
可以直接接收多余的参数。
接口与类型别名的区别
实际上,在大多数的情况下,接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别。
TS的核心原则之一是对值所具有的结构进行类型检查。而接口的作用就是为这些类型命名和为你的代码或第三方代码定义数据模型。
type(类型别名)会给一个类型起个新名字。type有时和interface很像,但是可以作用于原始值(基本类型),联合类型,元祖以及其他任何你需要手写的类型。起别名不会新建一个类型,它创建了一个新名字来引用那个类型。给基本类型起别名通常没什么用,尽管可以作为文档的一种形式使用。
Object / Functions
接口与类型别名两者都可以用来描述对象或函数的类型,但是语法不通。
interface
interface Point {
x: number,
y: number
interface SetPoint{
(x: number, y: number): void
Type alias
type point = {
x: number,
y: number
type SetPoint = (x: number, y: number) => void;
Other Types
与接口不同,类型别名还可以用于其他类型,如基本类型(原始值)、联合类型、元祖。
// primitive 原始的
type Name = string;
// object
type PartialPointX = {x: number};
type PartialPointY = {y: number};
// union 联合
type PartialPoint = PartialPointX | PartialPointY;
// tuple 元祖
type Data = [number, string];
// dom
let div = document.createElement('div');
type B = typeof div;
注意:接口可以定义多次,类型别名不可以。多次定义同名接口,会自动合并为单个接口。
两者的扩展方式不同,但并不互斥。接口可以扩展类型别名,类型别名可以扩展接口。
接口的扩展就是继承,通过extends实现。类型别名的扩展就是交叉类型,通过 & 实现。
// 接口扩展接口
interface PointX {
x: number
interface Point extends PointX {
y: number
// 类型别名扩展类型别名
type PointX = {
x: number
type Point = PointX & {
y: number
// 接口扩展类型别名
type PointX = {
x: number
interface Point extends PointX {
y: number
// 类型别名扩展接口
interface PointX {
x: number
type Point = PointX & {
y: number
当一个变量可以是任意类型,我们可以定义为any类型,但是any类型存在弊端较大,且失去了类型检查的意义。列举所有类型情况又不现实,这个时候就需要泛型来实现。
实现一个函数,接受任意类型,返回接收的参数
function identity<T>(arg: T): T { // 这个T是一个抽象类型,只有在调用的时候才能确定它的值
return arg;
其中T代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上T可以用任何有效名称代替。除了T之外,以下是常见泛型变量代表的意思:
K(Key): 表示对象中的键类型;
V(Value):表示对象中的值类型;
E(Element):表示元素类型;
定义泛型时<>中可以放置多个类型变量,用于扩展灵活性。
function identity<T, U>(value: T, message: U): T { // 调用时传入了两个类型,可以一一对应
return value;
identity<number, string>(68, 'message');
除了为类型变量显示设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。可以直接省略尖括号。
function identity(value: T, message: U): T {
return value;
identity(68, 'message'); // 自动根据传入参数推断类型,而不用显示定义类型。
当我们使用泛型而不对其进行任何约束时TS是会报错的:
function trace<T>(arg: T): T { // 使用泛型而没有约束,T理论上是任何类型,不同于any,直接访问属性或调用方法会报错,除非访问的是所有集合共有的
console.log(arg.size); // Error: Property 'size' doesn't exist on type 'T'
return arg;
解决上面的问题,就需要去对类型做约束,可以使用extends关键字去实现
interface Sizeable {
size: number
function trace<T extends Sizeable>(arg: T): T {
console.log(arg.size); // 此时传入的argue类型是继承Sizeable的,所以是拥有size类型的,也就是拥有size属性了。
return arg;
使用类型继承,而不是直接定义形参的类型,是为了可以接收更多类型,如果给形参定好了类型,传入别的类型时会丢失类型。
参考文章:https://juliangaramendy.dev/when-ts-generics/
泛型工具类型
为了方便开发者 TS中内置了一些常用的工具类型,比如Partial、Required、Readonly、Record和ReturnType等。
typeof
主要用途是在类型上下文中获取变量或者属性的类型。
interface Person {
name: string,
age: number
const sem: Person = { name:'xxx', age: 23 };
type sem = typeof sem; // type sem = Person
还可以用来获取函数对象的类型
function toArray(x: number): Array<number> {
return [x];
type Func = typeof toArray; // (x: number) => number[];
keyof
keyof操作符是在TS2.1版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。(就是拿着数据的key去当类型)
interface Person {
name: string,
age: number
type K1 = keyof Person; // 'name' | 'age'
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type k3 = keyof { [x: string]: Person }; // string | number
在TS中支持两种索引签名,数字索引和字符串索引:
interface StringArray {
// 字符串索引 -> keyof StringArray => string | number
[index: string]: string
interface StringArray1 {
// 数字索引 -> keyof StringArray1 => number
[index: number]: string
为了同时支持两种索引,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数字索引时,JS在执行索引操作时,会先把数字索引转换为字符串索引。所以 keyof { [x: string]: Person }的结果就会返回 string | number。
keyof也支持基本数据类型:
let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K3: "valueOf"
keyof 的作用
JS是一种高度动态的语言。有时在静态类型系统中捕获某些操作的语义可能会很棘手。
function prop(obj, key) {
return obj[key];
该函数接受 obj 和 key 两个参数,并返回对应属性的值。对象上的不同属性,可以具有完全不同的类型,我们甚至不知道obj长什么样。
对比在TS中定义prop函数
funtion prop(obj: object, key: string) {
return obj[key]; // Error Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
在TS中,为函数参数增加了类型校验,分别为 {} 和 string类型。但是上述代码在TS编译中会提示元素隐式拥有any类型, string类型的key不能用于{}类型
可以利用泛型来解决这个问题
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
使用了泛型和泛型约束,定义了T类型并使用了extends继承了object,说明T必须object类型的子类型,然后使用keyof获取T类型的所有键作为K的泛型约束,K类型就必须为 keyof T联合类型的子类型,(即K的值必须是T的键值)。
而当我们读取不存在的属性时,编译就会报错,阻止我们读取不存在的属性。
in 用来遍历枚举类型:
type Keys = 'a' | 'b' | 'c';
type Obj = {
[p in Keys]: any
}; // -> {a: any, b: any, c: any}
infer
在条件类型表达式中,可以在extends条件语句中使用 infer 声明一个待推断的类型变量并且对它进行使用。
先看ReturnType:获取函数返回值类型
const add = (x: number, y: number) => x+y;
type t = ReturnType<typeof add> // type t = number add函数类型推断返回类型为number
tips:不要滥用ReturnType工具类型,尽量手动标注函数返回值类型。
ReturnType 的实现源码:
type ReturnType<T extends (..args: any) => any> = T extends (...args: any) => infer R ? R : any
infer 的作用是让 TS 自己推断,并将推断的结果存储到一个类型变量中,infer 只能用于extends语句中。
type T0 = ReturnType<() => string> // string
type T1 = ReturnType<(s: string) => void> // void
type T2 = ReturnType<<T>() => T> // unknown
借助infer 实现元祖转联合类型
如:[string, number] -> string | number
type Flatten<T> = T extends Array<infer U> ? U : never;
type T0 = [string, number];
type T1 = Flatten<T0>; // string | number
解释:通过定义T0为元祖类型,通过泛型传递给 Flatten,元祖类型在一定条件下是可以赋值给数组类型的,所以Array<infer U>可以拿到 string | number,再将其返回,则这个类型拿到的是联合类型 string | number。
总结:infer理解起来比较抽象,实际上就是通过infer R 来拿到未知的返回值类型,如果存在则返回这个类型,不然返回默认设定的类型。注意的是需要extends语句中使用infer。
extends
有时我们定义的泛型不想过于灵活或者说想继承某些类型,可以通过extends关键字添加泛型约束。
利用索引类型,提前规范属性类型,可以在编译期间就提示读取未定义的属性错误。
interface Person {
name: string,
age: number
const person: Person = {
name: 'musion',
age: 35
// T[K]表示对象T的属性K所表示的类型
function getValue<T, K extends keyof T>(obj: T, keys: K[]): T[K][] { // 函数返回值类型 T[K][],即返回obj[key]拿到的值组成的类型数组
return keys.map(key => obj[key]);
getValue(person, ['name'])
解释:通过泛型T拿到Person类型的对象,K类型代表 Person类型的所有键类型,这样可以拿到所有键为K类型集合,返回值为 T[K][]
表示的是返回的是Person[key]的值的集合。
// 通过[]索引类型访问操作符,我们就能得到某个索引的类型
class Person {
name: string;
age: number
type MyType = Person['name'] // Person中的name的类型为 string, type MyType = string
根据旧的类型创建出新的类型,我们称之为映射类型。
interface Test {
name: string,
age: number
// 可以通过 +/- 来指定添加还是删除
// 映射一个新类型为 只读可选
type Options<T> = {
+readonly [p in keyof T] +?: T[p]
type New = Options<Test>
// type new = {
// readonly name?: string,
// readonly age?: number
Partial
Partial<T> 将类型的属性变成可选
tips:Partial<T>只支持处理第一层的属性,如果类型定义为多层,则需要手动进行递归处理。
// DeepPartial
type DeepPartial<T> = {
// 如果是object,则di'gui
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
type PartialedWindow = DeepPartial<T>; // 现在T上的所有属性都会变成可选
Required
Required将类型的属性变成必选。
// 定义
type Required<T> = {
[p in keyof T]-?: T[p]
// -? 代表移除 ? 这个modifier(修饰符)的标识
Readonly
Readonly<T>的作用是将某个类型所有属性变为只读属性,也就意味着这些属性不能被重新赋值。
// 定义
type Readonly<T> = {
readonly [p in keyof T]: T[p]
Pick 从某个类型中挑选一些属性出来。
// 定义
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
// 举例说明
interface Todo {
title: string,
description: string,
completed: boolean
type TodoPreview = Pick<Todo, 'title' | 'completed'>;
const todo: TodoPreview = {
title: 'xxxx',
completed: false
Record
Record<K extends keyof any, T>的作用是将 K 中所有的属性的值转化为T类型。
// 定义
type Record<K extends keyof any, T> = {
[p in K]: T;
// 举例说明
interface PageInfo {
title: string
type Page = "home" | "about" | "contact";
const x: Record<Page, PageInfo> = {
about: {title: "about"},
contact: {title: "contact"},
home: {title: "home"}
// 解释:将PageInfo作为类型,改造Page类型中的每一个属性的值为PageInfo类型,即修改Page的属性的类型为PageInfo(将某个类型的属性改造成指定类型)
ReturnType
用来得到一个函数的返回值类型。
// 定义,<>内的代码是先声明接收的泛型继承于函数类型,然后再赋值真正的类型,使用infer R 来提取函数返回值类型,如果有则返回提取到的类型,否则返回any类型
type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R ? R : any;
// 举例说明
type Func = (value: number) => string
const foo:ReturnType<Func> = "1"; // foo类型为string,故可以赋值为“1”
Exclude
Exclude<T, U>的作用是将某个类型中属于另一个类型移除掉。
// 定义
type Exclude<T, U> = T extends U ? never : T;
// 解释:如果T能赋值给U类型的话,那么就会返回never类型,否则返回T类型。
// 举例说明
type T0 = Exclude<'a'|'b'|'c', 'a'>; // 'b' | 'c'
type T1 = Exclude<'a'|'b'|'c', 'a'|'b'>; //'c'
type T2 = Exclude<string | number | (() => void), Function>; // string|number
Extract
Extract<T, U>的作用是从T中提取出U。
// 定义 跟 Exclude相反,T能赋值给U就保留,否则为never
type Extract<T, U> = T extends U ? T : never
// 举例说明
type T0 = Extract<'a'|'b'|'c'|, 'a'|'f'>; // 'a'
type T1 = Extract<string | number | (() => void), Function>; // () => void
Omit<T, K extends keyof any>的作用是使用T类型中除了K类型的所有属性,来构造一个新的类型.
// 定义
type Omit<T,K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 解释: 从右往左看,利用Exclude<keyof T, K>先筛选出T属性中不包含K类型的,返回类型集合,再通过Pick筛选出T中包含的前面返回的类型集合,构造一个新的类型.
// 举例说明
interface Todo {
title: string,
description: string,
completed: boolean
type TodoPreview = Omit<Todo, "description">
const todo: TodoPreview = {
title: 'xxx',
completed: false
// 解释:定义接口类型,创建新type为从接口类型中剔除"description"属性的结果
NonNullable
NonNullable<T>的作用是用来过滤类型中的null 及 undefined类型.
// 定义
type NonNullable<T> = T extends null | undefined ? never : T;
// 举例说明
type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]
Parameters
Parameter<T> 的作用是用于获得函数的参数类型组成的元组类型.
// 定义
type Parameters<T extends (...args: any[]) => any> = T extends (
...args: infer P
) => any ? P : never
// 解释:这个泛型中接收的类型肯定是要继承函数类型的,然后通过infer P拿到形参的类型,如果T继承于(..args: infer P) = > any,则返回拿到的形参类型,否则返回never
// 注意: infer P在这里的作用是定义了形参待推断类型的变量
// 举例说明
type A = Parameters<() => void>; // []
type B = Parameters<typeof Array.isArray>; // [any]
type C = Parameters<typeof parseInt>; // [string, (number | undefined)?]
type D = Parameters<typeof Math.max>; // number[]
装饰器(目前是实验性功能)
随着TypeScript和ES6里引入了类,部分场景需要额外的特性来支持标注或修改类及其成员。装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。
想使用装饰器需要开启配置
// tsconfig.json
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
// 或者使用命令编译文件
tsc 目标文件 ES5 --experimentalDecorators
tsconfig.json
tsconfig.json介绍
tsconfig.json是 TypeScript项目的配置文件。如果一个目录下存在一个tsconfig.json文件,那么往往意味着这个目录就是TypeScript项目的根目录。
tsconfig.json包含TypeScript编译的相关配置,通过更改编译配置项,我们可以让TypeScript编译出ES6、ES5、node的代码。
tsconfig.json重要字段
files - 设置要编译的文件的名称;
include - 设置需要进行编译的文件,支持路径模式匹配;
exclude - 设置无需进行编译的文件,支持路径模式匹配;
compilerOptions - 设置与编译流程相关的选项。
compilerOptions选项
"compilerOptions": {
// 基本选项
"target": "es5", // 指定ECMAScript 目标版本
"module": "commonjs", // 指定使用模块
"lib": [], // 指定要包含在翻译中的库文件
"allowJs": true, // 允许编译js文件
"checkJs": true, // 报告js文件中的错误
"jsx": preserve, // 指定jsx代码的生成:‘preserve’
"declaration": true, // 生成相应的 '.d.ts'文件
"sourceMap": true, // 生成相应的.map文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件做为单独的模块(与“ts.transpileModule”类似)
// 严格的类型检查选项
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any 类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 "use strict"
// 额外的检查
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告switch语句的fallthrough错误(即 不允许switch的case语句贯穿)
// 模块解析选项
"moduleResolution": "node", // 选择模块解析策略: 'node'(Node.js) or 'classic'(TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导出
// Source Map Options
"sourceRoot": "./", // 指定调试器应该找到TypeScript文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 sourcemaps文件,而不是将sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与sourcemaps生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
// 其他选项
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
编写高效TS代码的一些建议
1、可以通过类型推断的数据,不要手动再添加类型注解:
const x:number = 1(此时的number类型没有必要)
2、定义复杂数据类型时,实际开发中默认值可能与类型不符,初始化可推荐类型断言来避免类型检查:
// 类型断言、本质上就是告诉编译器我知道这个操作意味着什么,我明确可以将obj当成Test对待
interface Test {
a: number,
b: string
const obj: Test = {} as Test // 通过添加 as Test来避免赋值时的类型检查
3、不可变数据,有const定义、defineProperty两种方式实现,const遇到修改会报错,打断代码执行,而defineProperty就算修改不成功也不报错,没有提示,相对都不太友好。
// 1.使用 Union Type 语法,定义数据的具体类型。此时定义的globalData不能进行修改
interface IGlobal {
scene: 'android' | 'ios' | 'symbian'
let globalData: IGlobal = {
scene: 'android'
// 2.使用readonly属性,可以加在属性 scene上,也可以加载 IGlobal上。
{ readonly scene: 'android' | 'ios' | 'symbian' }
let globalData: Readonly<IGlobal> = {scene: 'ios'}
4、扩展全局变量
很多时候,开发中需要往window对象上挂载数据,TS中直接使用,不定义扩展,编译时会报错。当我们想扩展全局变量,如Array、String、Window等时,需要使用 declare global {} 的语法来实现。
在使用这个语法去扩展全局变量时,需要导出一个空对象,确保它时一个模块。
export {} // 添加这一行可以使得该文件被视为一个模块,而非脚本
declare global {
const xx: any
interface Window {
somethingInWindow: number[]
interface Array<T> {
test(): volid
尽量减少重复代码
示例代码:定义存在重复属性接口时
// 在基础接口Person上实现PersonWithBirthDate
interface Person {
firstName: string;
lastName: string;
// 错误示例,重新定义属性
interface PersonWithBirthDate {
firstName: stirng;
lastName: string;
birth: Date
// 正确示例
// 1、使用extends继承
interface PersonWithBirthDate extends Person {
birth: Date;
// 2、使用交叉运算符 &
interface PersonWithBirthDate = Person & { birth: Date };
示例代码:需要为数据对象配置类型
const init_options = {
width: 640,
height: 480,
color: "#00FF00",
label: "VGA"
// 错误示例:直接按照属性配置
interface Options {
width: number,
height: number,
color: string,
label: string
// 推荐做法:使用 typeof操作符
type Options = typeof init_options;
示例代码:类似多个函数拥有相同的类型签名
function get(url: string, opts: Options): Promise<Response> { /* ... */ }
function post(url: string, opts: Options): Promise<Response> { /* ... */ }
// 提取函数签名
type HTTPFunction = (url: string, opts: Options) => Promise<Response>;
const get: HTTPFunction = (url, opts) => { /* ... */ };
const post: HTTPFunction = (url, opts) => { /* ... */ };
示例代码:使用更精确的类型替代字符串类型
// 当属性有指定值范围时,使用字面量类型定义,而不是string类型
status: 'on' | 'off'
// 当属性为日期类型时,可以定义类型为日期类型,而不是string类型
date: Date
定义的类型总是表示有效的状态
TypeScript中的模块:
在TypeScript中编写基于模块的代码时,需要考虑三件事:
语法:要用什么语法来导入和导出东西?
模块解析:模块名称(或路径)与磁盘上的文件有什么关系?
模块输出目标:发出的Javascript模块应该是什么样子?
TypeScript 特定的 ES模块语法
可以使用与Javascript值相同的语法导出和导入类型:
// @filename: animal.ts
export type Cat = { breed: string, yearOfBirth: number };
export interface Dog {
breeds: string[];
yearOfBirth: number;
// @filename: app.ts
import { Cat, Dog } from './animal.js'
type Animals = Cat | Dog;
TypeScript import 使用两个概念扩展了语法,用于声明类型的导入
import type:这是一个只能导入类型的导入语句。
// @filename: animal.ts
export type Cat = { breed: string, yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
// @filename: valid.ts
import type { Cat, Dog } from './animal.js';
export type Animals = Cat | Dog;
// @filename: app.ts
import type { CreateCatName } from './animal.js';
const name = createCatName();
内联type导入:TypeScript4.5还允许为单个导入添加前缀,type以指示导入的引用是一种类型:
// @filename: app.ts
import { createCatName, type Cat, type Dog } from './animal.js';
export type Animals = Cat | Dog;
const name = createCatName();
这些一起允许像 Babel、swc 或 esbuild 这样的非TypeScript转译器知道可以安全地删除哪些导入。