TypeScript基本理解和语法糖
typescript是对于Javascript的一种安全封转,因此大部分对于typescript的操作都要转换成JavaScript进行处理,并且会多出一些d.ts的类型定义文件以保证类型安全性的扩展。
对于ts来说,有如下这一些基本的结构会出现:
- 声明部分:包括类型声明、接口声明等。
- 变量声明:包括
let
,const
和var
的使用。 - 函数声明:包括普通函数和箭头函数。
- 类声明:用于定义类及其成员。
- 接口与类型别名:描述类型的结构。
- 模块化:通过
import
和export
组织代码。 - 类型断言:强制类型转换。
- 泛型:使代码具备更多的复用性。
- 注释:增加代码的可读性。
- 类型推断:自动推断类型。
- 类型守卫:缩小类型范围。
- 异步编程:支持
async/await
。 - 错误处理:通过
try/catch
进行错误捕捉。
我们按章节处理这些基本的定义,其实在Java这种更加OOP的语言中,以上情况也是常见的,不过作为多线程的语言,Java和js的核心理念还是有所区别的,并且js,ts在类型上是独特于Java的一种动态类型,ts在保证安全性的情况下有更安全的类型扩展,比Python的超级弱类型更加的容易管理一些,不过也会出现一些静态检查上的分析误差,这些都要在开发的时候要注意。
一、声明部分(Declarations)
类型声明:TypeScript 是一种静态类型的语言,可以通过类型声明来定义变量、函数、类等的类型。类型声明可以帮助代码更具可维护性和可读性。
接口声明:用于定义对象的结构,包括对象的属性和方法。
interface Person {
name: string;
age: number;
}
变量声明:可以使用let, const,var三种方式完成对变量的声明,各自有区分,具体的场景如下
推荐使用 let 和 const,var 用法不再推荐。
let age: number = 25;
const pi: number = 3.14;
函数声明:TypeScript 允许声明带有类型注解的函数,包括参数类型和返回值类型。
function greet(name: string): string {
return "Hello, " + name;
}
箭头函数:TypeScript 同样支持 ES6 的箭头函数,使用简洁的语法来声明函数。
const greet = (name: string): string => "Hello, " + name;
类声明:TypeScript 提供对面向对象编程的支持,允许定义类和类的方法、属性。
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name}`;
}
}
接口(Interface):用于描述对象的形状,接口可以继承和扩展。
interface Animal {
name: string;
sound: string;
makeSound(): void;
}
类型别名(Type Alias):允许为对象类型、联合类型、交叉类型等定义别名。
type ID = string | number;
泛型:泛型允许在定义函数、接口或类时不指定具体类型,而是使用占位符,让用户在使用时传入具体类型。泛型能够增加代码的复用性和类型安全性。
function identity<T>(arg: T): T {
return arg;
}
类型推断:TypeScript 在某些情况下会自动推断变量的类型。例如,在声明变量并赋值时,TypeScript 会推断出该变量的类型。
let num = 10; // TypeScript 推断 num 为 number 类型
类型守卫
TypeScript 提供了类型守卫(如 typeof 和 instanceof),用于在运行时缩小变量的类型范围
function isString(value: any): value is string {
return typeof value === 'string';
}
类型擦除
Javascript有非常变态的类型擦除,在 JavaScript/TypeScript 中,对象不是单一的精确类型。例如,如果我们构造一个满足接口的对象,我们可以在需要该接口的地方使用该对象,即使两者之间没有声明性关系。
interface Pointlike {
x: number;
y: number;
}
interface Named {
name: string;
}
function logPoint(point: Pointlike) {
console.log("x = " + point.x + ", y = " + point.y);
}
function logName(x: Named) {
console.log("Hello, " + x.name);
}
const obj = {
x: 0,
y: 0,
name: "Origin",
};
logPoint(obj);
logName(obj);
TypeScript 的类型系统是结构性的,而不是名义性的:我们可以将 obj
用作 Pointlike
,因为它具有 x
和 y
属性,这两个属性都是数字。类型之间的关系由它们包含的属性决定,而不是由它们是否使用某种特定关系声明。
因此,推荐我们用集合论的观点去看类型(types),我们可以将 obj
视为 Pointlike
值集和 Named
值集的成员。TypeScript 的类型系统也没有具体化 :运行时没有任何东西可以告诉我们 obj
是 Pointlike
。事实上,Pointlike
类型在运行时不以任何形式存在。
BUT REMEMBER:
Remember: Type annotations never change the runtime behavior of your program.
请记住 :类型注释永远不会更改程序的运行时行为。
Empty Types 空类型
The first is that the empty type seems to defy expectation:
首先是 empty 类型似乎出乎意料:
class Car {
drive() {
// hit the gas
}
}
class Golfer {
drive() {
// hit the ball far
}
}
// No error?
let w: Car = new Golfer();
TypeScript 通过查看提供的参数是否为有效的 Empty
来确定此处对 fn
的调用是否有效。它通过检查 { k: 10 }
和类 Empty { }
的结构来实现此目的。我们可以看到 { k: 10 }
具有 Empty
的所有属性,因为 Empty
没有属性。因此,这是一个有效的决定!
这可能看起来令人惊讶,但它最终与名义上的 OOP 语言中强制执行的关系非常相似。子类不能删除其基类的属性,因为这样做会破坏派生类与其基类之间的自然子类型关系。结构类型系统只是通过根据具有兼容类型的属性来描述子类型来隐式地标识这种关系。
导致会有如下的糟糕情况出现(作为Java程序员的我认为糟糕)
class Car {
drive() {
// hit the gas
}
}
class Golfer {
drive() {
// hit the ball far
}
}
// No error?
let w: Car = new Golfer();
反射(reflection)
OOP 程序员习惯于能够查询任何值的类型,甚至是通用值:
static void LogType<T>() {
Console.WriteLine(typeof(T).Name);
}
因为 TypeScript 的类型系统被完全擦除,所以有关泛型类型参数的实例化等信息在运行时不可用。
JavaScript 确实有一些有限的原语,如 typeof
和 instanceof
,但请记住,这些运算符仍在处理类型擦除输出代码中存在的值。例如,typeof (new Car())
将是 “object”
,而不是 Car
或 “Car”。
类型 | 描述 | 示例 |
---|---|---|
string | 表示文本数据 | let name: string = "Alice"; |
number | 表示数字,包括整数和浮点数 | let age: number = 30; |
boolean | 表示布尔值 true 或 false | let isDone: boolean = true; |
array | 表示相同类型的元素数组 | let list: number[] = [1, 2, 3]; |
tuple | 表示已知类型和长度的数组 | let person: [string, number] = ["Alice", 30]; |
enum | 定义一组命名常量 | enum Color { Red, Green, Blue }; |
any | 任意类型,不进行类型检查 | let value: any = 42; |
void | 无返回值(常用于函数) | function log(): void {} |
null | 表示空值 | let empty: null = null; |
undefined | 表示未定义 | let undef: undefined = undefined; |
never | 表示不会有返回值 | function error(): never { throw new Error("error"); } |
object | 表示非原始类型 | let obj: object = { name: "Alice" }; |
union | 联合类型,表示可以是多种类型之一 | `let id: string|number |
unknown | 不确定类型,需类型检查后再使用 | let value: unknown = "Hello"; |
二、类型
在声明一块中已经强调了大部分的类型相关的内容,JavaScript也是需要“先声明后使用”的,这点非常重要。
基元类型(PRIMITIVES)
JavaScript 有三个非常常用的原语 :string
、number
和 boolean
。每个在 TypeScript 中都有相应的类型。正如你所料,如果你对这些类型的值使用 JavaScript typeof
运算符,这些名称与你看到的名称相同:
Array
要指定数组的类型,如 [1, 2, 3]
,你可以使用语法 number[]
;此语法适用于任何类型(例如 string[]
是字符串数组,依此类推)。您可能还会看到它写成 Array<number>
,这意味着相同的内容。在介绍泛型时,我们将了解有关语法 T<U>
的更多信息。
Note that [number]
is a different thing; refer to the section on Tuples.
请注意,[number]
是另一回事;请参阅 Tuples 部分。
any
TypeScript 还有一个特殊的类型 any
,当你不希望特定值导致类型检查错误时,你可以使用它。
https://www.allthingstypescript.dev/p/why-avoid-the-any-type-in-typescript
Object Types对象类型
除了基元之外,您遇到的最常见的类型是对象类型 。这指的是任何具有属性的 JavaScript 值,这几乎是所有属性!要定义对象类型,我们只需列出其属性及其类型
function printCoord(pt: { x: number; y: number }) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });
在这里,我们使用具有两个属性(x
和 y
)的类型对参数进行批注,这两个属性都是 number
类型。您可以使用 ,
或 ;
来分隔属性,并且最后一个分隔符是可选的。
Optional Properties 可选属性
对象类型还可以指定其部分或全部属性是可选的 。为此,请在属性名称后添加 ?
:
function printName(obj: { first: string; last?: string }) {
// ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
在 JavaScript 中,如果你访问一个不存在的属性,你将得到值 undefined
,而不是运行时错误。因此,当你从可选属性中读取时,你必须在使用它之前检查 undefined
。
function printName(obj: { first: string; last?: string }) {
// Error - might crash if 'obj.last' wasn't provided!
console.log(obj.last.toUpperCase());
!!!!! 'obj.last' is possibly 'undefined'.
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}
// A safe alternative using modern JavaScript syntax:
console.log(obj.last?.toUpperCase());
}
Union Types联合类型
TypeScript 的类型系统允许您使用各种运算符从现有类型中构建新类型。现在我们已经知道如何编写一些类型,是时候开始以有趣的方式组合它们了。
比如下面这个示例就又可以对字符串又可以对数字进行操作
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
union 成员的分隔符允许在第一个元素之前使用,因此您也可以编写以下内容:
function printTextOrNumberOrBool(
textOrNumberOrBool:
| string
| number
| boolean
) {
console.log(textOrNumberOrBool);
}
但是务必注意:TypeScript 仅在对联合的每个成员都有效时才允许作。例如,如果你有 union string | number
,则不能使用仅在 string
上可用的方法。
所以这更像是一种语法层面的设计模式运用。给每个类型自动套上一层适配器,然后就可以一起使用了。
解决方案是缩小与代码的联合,就像在没有类型注释的 JavaScript 中一样。 当 TypeScript 可以根据代码的结构为值推断出更具体的类型时,就会发生收缩 。
function printId(id: number | string) {
if (typeof id === "string") {
// In this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// Here, id is of type 'number'
console.log(id);
}
}
另一类则是使用类似类型库中函数来判断一样的:
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// Here: 'x' is 'string[]'
console.log("Hello, " + x.join(" and "));
} else {
// Here: 'x' is 'string'
console.log("Welcome lone traveler " + x);
}
}
所以理论上来说,JavaScript和typescript中的union类型是类型的交集,使用其共同的属性进行工作,并且在必要情况下对类型进行narrowing
https://www.typescriptlang.org/docs/handbook/2/everyday-types.html
类型联合似乎具有这些类型属性的交集 ,这可能会令人困惑。这不是偶然的 – union 这个名字来自类型理论。unionnumber | string
是通过获取每种类型的值的 union 组成的。请注意,给定两个集合,每个集合都有相应的事实,只有这些事实的交集适用于集合本身的并集 。例如,如果我们有一个房间是戴帽子的高个子,另一个房间是戴着帽子的讲西班牙语的人,那么把这些房间合并后,我们对每个人的唯一了解就是他们一定戴着帽子。
类型别名(Type Aliases)
其实很像是有名的union,或者把“像”去掉也对,类型别名就是 – 任何类型的名称 。类型别名的语法为:
type Point = {
x: number;
y: number;
};
// Exactly the same as the earlier example
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 100, y: 100 });
类型别名和接口(Interface)的区别
Interface | Type |
---|---|
Extending an interfaceinterface Animal { name: string; } } bear.name; bear.honey; | Extending a type via intersectionstype Animal = { name: string; } } bear.name; bear.honey; |
Adding new fields to an existing interfaceinterface Window { title: string; } | A type cannot be changed after being createdtype Window = { title: string; } |
类型断言
有时,你会知道typescript本身无法知道的有关值类型的信息,比如下面这个例子
例如,如果你正在使用 document.getElementById,TypeScript
只知道这将返回某种 HTMLElement
,但你可能知道你的页面将始终具有具有给定 ID 的 HTMLCanvasElement
。
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
与类型注释一样,类型断言由编译器删除,不会影响代码的运行时行为。
并且下面这种尖括号语法也是被支持的
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
TypeScript 只允许类型断言转换为更具体或不太具体的类型版本。此规则可防止“不可能的”强制行为,例如什么string转number的“不可能行为”是不被支持的
再叙类型守卫
假设我们有这样一个padLeft函数
function padLeft(padding: number | string, input: string): string {
throw new Error("Not implemented yet!");
}
如果 padding
是一个数字
,它会将其视为我们想要在 input
前面添加的空格数。如果 padding
是一个字符串
,它应该只在 input
前面加上 padding
。让我们尝试实现 padLeft
何时传递数字
进行填充
的逻辑。
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
在我们的 if
检查中,TypeScript 看到 typeof padding === “number”
并将其理解为一种称为类型守卫的特殊代码形式。TypeScript 遵循我们的程序可以采用的可能执行路径来分析给定位置最具体的可能值类型。它着眼于这些特殊检查(称为类型保护 )和赋值,将类型细化为比声明的更具体的类型的过程称为 narrowing。在许多编辑器中,我们可以观察这些类型的变化,我们甚至会在示例中这样做。
但比如说有时候我们会写出typeof B === “string” ,但actually typeof null ===”string”
具有足够经验的用户可能不会感到惊讶,但并不是每个人都在 JavaScript 中遇到过这种情况;幸运的是,TypeScript 让我们知道 strs
只缩小到 string[] | null
,而不仅仅是 string[]。
因此在typescript中,缩小范围是一项极其重要的类型操作,好比cpp中习惯于手动垃圾管理、生命周期管理、类型体操等等,在typescript中,narrowing就是最重要的trick,其中无论是类型守卫narrowing,还是使用===,in等特殊运算符来narrowing,都是必要的操作以支持更加丰富的后续操作(太大的类型无法访问一些特定参数且越来越难控制)具体有哪些典型的narrowing手段可以看这里:
https://www.typescriptlang.org/docs/handbook/2/narrowing.html
可区分联合
可能会遇到这样问题:
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
请注意,我们使用字符串文本类型的联合:“circle”
和 “square”
来告诉我们应该将形状分别视为圆形还是方形。通过使用 “circle” |“square”
而不是 string
,我们可以避免拼写错误的问题。
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
'shape.radius' is possibly 'undefined'.
}
在 strictNullChecks
下,这给我们带来了一个错误 – 这很合适,因为可能没有定义 radius
。但是,如果我们对 kind
属性执行适当的检查呢?
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
'shape.radius' is possibly 'undefined'.
}
}
嗯,TypeScript 仍然不知道在这里做什么。我们已经达到了一个点,我们比类型检查器更了解我们的值。我们可以尝试使用非 null 断言(shape.radius
后面的 !
)来表示 radius
肯定存在。
这种 Shape
编码的问题在于,类型检查器无法根据 kind
属性知道 radius
或 sideLength
是否存在。我们需要将我们所知道的传达给类型检查器。考虑到这一点,让我们再来定义一下 Shape
。
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
我们再次尝试检查 kind
属性
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
(parameter) shape: Circle
}
}
这样就消除了错误!当联合中的每个类型都包含具有 Literal 类型的公共属性时,TypeScript 会将其视为可区分联合 ,并且可以缩小联合的成员范围。
函数Functions
描述函数的最简单方法是使用函数类型表达式 。这些类型在语法上类似于箭头函数:
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
当然,我们可以使用 type alias 来命名一个函数类型:
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
调用签名(Call Signatures)
作为一门类型语言,typescript中的函数还有一个神奇特性,你可以把函数概括出一种类型,函数可以有其属性,并且此时函数的参数也是属性的一部分。但是,函数类型表达式语法不允许声明属性。如果我们想用属性来描述可调用的东西,我们可以在对象类型中编写一个调用签名 :
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
function myFunc(someArg: number) {
return someArg > 3;
}
myFunc.description = "default description";
doSomething(myFunc);
构造签名(Construct Signature)
typescript函数特别的另一点是可以使用new运算符调用,也被称为构造函数,通常会创建一个新对象
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
某些对象(如 JavaScript 的 Date
对象)可以在有或没有 new
的情况下调用。您可以任意组合同一类型的 call 和 construct 签名:
