侧边栏壁纸
博主头像
Setyg的博客

当秋风染红落叶的时候,就是新生~

  • 累计撰写 86 篇文章
  • 累计创建 66 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

TypeScript 基本数据类型梳理与实践

Setyg
2026-09-21 / 0 评论 / 0 点赞 / 2 阅读 / 0 字

TypeScript 的类型注解只活在编译期,编译成 JavaScript 之后全部被擦除,运行时不存在类型检查。理解这一点,后面很多「为什么编译过了还是崩」的问题就顺了。本文按基础类型的顺序把声明写法、边界行为和各类型特有的坑过一遍。

一、声明语法与类型推断

// 关键字 变量名: 数据类型 = 具体值;
let isDone: boolean = false;
let count: number = 42;
let message: string = 'Hello TypeScript!';

字段名后面的冒号是必须写的,写错位置或漏写都会直接报类型错误,而不是运行时才出问题。

但也不是每处都要标注——TypeScript 会自动推断:

let a = 5;            // 推断为 number
let b = 'hello';      // 推断为 string
const c = 'hello';    // 推断为字面量类型 'hello'(const 不会被重新赋值,可以收得更窄)

// 有初始值时,标注通常是多余的
let d: number = 5;    // 冗余
// 没有初始值、或者推断结果过宽时才需要标注
let e: number;        // 此时必须标注,否则是 any

注意 let 和 const 的推断结果不同:let c = 'hello' 得到 string,const c = 'hello' 得到 'hello'。不需要重新赋值的变量一律用 const,能拿到更精确的类型。

二、布尔型(boolean)

let isDone: boolean = false;
let hasPermission: boolean = true;

function isAdmin(): boolean {
  return true;
}

只有 true 和 false 两个字面量。 0、''、null 这些 JS 里的假值不能赋给 boolean,这是和 JS 隐式转换习惯最容易冲突的地方。

三、数字型(number)

let decimal: number = 6;
let hex: number = 0xf00d;      // 十六进制
let binary: number = 0b1010;   // 二进制
let octal: number = 0o744;     // 八进制
let float: number = 3.14;
let negative: number = -100;
let scientific: number = 1.23e10;

let notANumber: number = NaN;
let infinity: number = Infinity;

四种进制的写法类型都是同一个 number——TypeScript(和 JavaScript)没有 int、float、double 的区分,整数和浮点共用一个类型。这一点从 Java / C# 转过来时最容易先入为主。

另外 NaN 和 Infinity 的类型也是 number,所以类型系统拦不住它们,判空得靠 Number.isNaN()。

超出安全整数范围(Number.MAX_SAFE_INTEGER,即 2^53 - 1)时需要 bigint(TypeScript 3.2 起,编译目标需 ES2020 及以上):

let big: bigint = 9007199254740993n;
// big + 1           // ❌ bigint 和 number 不能混合运算,必须写成 big + 1n

四、字符串型(string)

let color: string = 'blue';
let message: string = "Hello";

// 模板字符串:支持插值和多行
const fullName: string = 'John Doe';
const age: number = 30;
const info: string = `My name is ${fullName} and I am ${age} years old.`;

const multiLine: string = `This is a
multi-line string
in TypeScript.`;

单引号、双引号、反引号都是 string,类型层面没有区别。团队里一般靠 ESLint 统一引号风格。

还有一个用得少但值得认识的原始类型 symbol:

const key: symbol = Symbol('key');
const unique: unique symbol = Symbol('unique');   // 每个 unique symbol 都是独立类型

五、空值(null)与未定义(undefined)

let nullableNumber: number | null = 10;
let maybeUndefined: string | undefined;

// 直接声明单个类型也可以,但基本没有意义
let emptyValue: null = null;
let notDefined: undefined = undefined;

实际项目里几乎不会单独用 null 或 undefined 作为类型,而是和别的类型组成联合类型(number | null)来表示「可能没有值」。

这里的关键开关是 strictNullChecks(属于 strict 的一部分,新建项目默认开启):

行为 strictNullChecks: true strictNullChecks: false
null / undefined 能否赋给其他类型 不能,必须显式写进联合类型 可以,它们是所有类型的子类型
let s: string = null 编译报错 通过
空值检查的实际效果 真正有用 形同虚设,等于没开

开启后还有一条容易撞上的规则——严格模式下读到未赋值的变量会报错:

let maybeUndefined: string | undefined;
console.log(maybeUndefined);   // ❌ 严格模式下:变量在赋值前被使用
maybeUndefined = undefined;    // ✅ 显式赋 undefined 之后才可读

另外,tsconfig.json 里的 exactOptionalPropertyTypes(4.4+)会进一步区分「属性缺失」与「属性存在但值为 undefined」,写接口类型时值得留意。

选 null 还是 undefined:TypeScript 官方风格指南建议统一用 undefined 表示「没有值」,null 留给确实需要表达「空」语义的对外接口(比如后端返回的 null)。两套混用会让判空逻辑到处分叉。

六、数组(Array)

// 写法一:类型后跟 []
let numbers: number[] = [1, 2, 3];
let strings: string[] = ['Hello', 'World'];

// 写法二:泛型语法,两者完全等价
let moreNumbers: Array<number> = [1, 2, 3];

// 联合类型数组
let mixedArray: (number | string)[] = [1, 'Two', 3, 'Four'];

// 只读数组
let readonlyArray: readonly number[] = [1, 2, 3];
// readonlyArray.push(4);      // ❌ 只读数组没有变更方法
// readonlyArray[0] = 9;       // ❌

// 多维数组
let matrix: number[][] = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

// 元素是对象的数组
let users: { id: number; name: string }[] = [{ id: 1, name: 'Alice' }];

两个需要留意的点:

  • readonly number[] 与 ReadonlyArray<number> 等价,但不能与 number[] 互相赋值:只读数组不能赋给可变数组(丢了只读保证)。反向赋值是允许的,这也是「函数参数尽量声明为只读数组」的理由。
  • 下标越界不会报错:numbers[99] 的类型是 number,运行时却拿到 undefined。这是类型系统在这里的一个已知漏洞,开启 noUncheckedIndexedAccess 后下标访问的类型会变成 number | undefined,逼你判空。

七、元组(Tuple)

元组表示「元素数量和每个位置的类型都已知」的数组:

// 基本元组:长度和每个位置的类型都要匹配
let x: [string, number];
x = ['hello', 10];      // ✅
// x = [10, 'hello'];   // ❌ 类型不匹配
// x = ['hello'];       // ❌ 长度不匹配

// 声明后再赋值也可以,但类型必须严格对上
let user: [string, number, boolean];
user = ['Alice', 25, true];

三个进阶写法:

// 可选元素:TypeScript 3.0 起支持
let optionalTuple: [string, number?];
optionalTuple = ['Hello'];          // ✅
optionalTuple = ['Hello', 123];     // ✅

// 剩余元素:3.0 起
let restTuple: [string, ...number[]];
restTuple = ['a'];                  // ✅
restTuple = ['a', 1, 2, 3];         // ✅

// 具名标签:4.0 起,纯可读性提升,不影响类型
let labeled: [name: string, age?: number];
labeled = ['Alice', 25];

// 只读元组
const readonlyTuple: readonly [string, number] = ['Hello', 123];

元组最常见的用途是函数返回多个值,配合解构很自然:

function parseName(input: string): [string, string] {
  const [first, ...rest] = input.split(' ');
  return [first, rest.join(' ')];
}

const [firstName, lastName] = parseName('John Doe');

元组有个反直觉的地方:push、pop 这类方法在元组上仍然可用,而且 push 只检查元素类型、不检查长度:

let pair: [string, number] = ['a', 1];
pair.push(2);         // ✅ 编译通过,但运行时长度已经变成 3
// pair.push(true);   // ❌ 元素类型不对,这里会报错

所以元组适合表达「固定结构的数据」,不适合当可变集合用。真要锁定长度,得加 readonly。

八、枚举(Enum)

enum Color { Red, Green, Blue }
let c: Color = Color.Red;

console.log(Color.Red);   // 0
console.log(Color.Green); // 1
console.log(Color.Blue);  // 2
console.log(Color[0]);    // 'Red' —— 数字枚举支持反向映射

数字枚举成员默认从 0 开始自增,也可以显式指定起点或逐个赋值:

enum Status { Pending = 1, Running, Done }   // 1, 2, 3

// 字符串枚举:没有自增、也没有反向映射
enum Direction { Up = 'UP', Down = 'DOWN' }
console.log(Direction.Up);      // 'UP'
// console.log(Direction['UP']); // ❌ 字符串枚举不支持反向映射

关于枚举有三个必须知道的点:

  • 它是少数会生成运行时代码的类型特性。写 enum Color { Red },编译产物里会多出一个真实的 Color 对象;而 interface、type、类型注解编译后全部消失。所以枚举不是「零成本」的。
  • const enum 会在编译期把引用处直接内联成字面量、不生成对象,但 babel / esbuild 这类只做单文件转译、不读类型信息的工具无法内联它,需要额外配置(preserveConstEnums 等)。这也是很多团队直接禁用 const enum 的原因。
  • 枚举的取值是「运行时真实存在的对象属性」,所以不能只当类型用——比如 Color 既能做类型也能做值,而 interface 只能做类型。

想让取值更可控,可以用 as const 对象 + 联合类型替代(这也是现在社区比较推荐的做法):

const Color = {
  Red: 'RED',
  Green: 'GREEN',
  Blue: 'BLUE'
} as const;

// 从对象反推出联合类型:'RED' | 'GREEN' | 'BLUE'
type Color = typeof Color[keyof typeof Color];

function paint(color: Color) { /* ... */ }
paint(Color.Red);     // ✅
// paint('RED');      // ✅ 字符串字面量也能通过,因为类型就是它
// paint('PINK');     // ❌

这样做没有运行时代码开销,取值就是普通的字符串,和 JSON 交互时也不用额外转换。

九、any、unknown、void、never

这四个容易混在一起,先看它们的赋值关系:

类型 能赋给它 它能赋给 能否直接使用
any 任何类型 任何类型 可以,不做任何检查
unknown 任何类型 只能赋给 unknown 和 any 不可以,必须先收窄类型
never 只有 never 任何类型 它是「永不存在的值」,本身不可用
void undefined(严格模式下) any、unknown 只用于函数返回值位置

any:任意类型

let anything: any = 'This can be anything!';
anything = 42;
anything = true;

anything.foo.bar();        // 编译通过,运行时才崩
const n: number = anything; // 编译通过

any 关闭了该值的全部检查,而且会传染:any 参与运算的结果还是 any,类型检查会沿着调用链一路失效。项目里应该开启 noImplicitAny(属于 strict),至少保证「没写类型」不会静默变成 any。

unknown:未知类型

let value: unknown = 'hello';

// value.toUpperCase();          // ❌ 直接调用成员会报错
if (typeof value === 'string') {
  value.toUpperCase();            // ✅ 收窄之后才可用
}

// 或者显式断言
const len: number = (value as string).length;

unknown 是 any 的安全版本:能接住任何值,但使用前必须证明它是什么。接外部数据(接口响应、JSON.parse、postMessage)时优先用 unknown,不要用 any。

void:无返回值

function warnUser(): void {
  console.log('This is my warning message');
}

void 和 undefined 不是一回事:void 表示「调用方不应该使用返回值」。这一点在回调参数位置上有个特别的行为——() => void 可以接受返回任意值的函数:

function run(cb: () => void) { cb(); }

run(() => 123);              // ✅ 允许,返回值会被忽略
// function f(): void { return 123; }   // ❌ 直接声明返回 void 却返回值,报错

这个规则容易被误认为是类型漏洞,其实是有意为之:[1,2,3].forEach(x => arr.push(x)) 能通过,就是因为 forEach 的回调声明为 => void,而 push 返回 number。

never:永不返回

// 抛异常的函数:不会有正常返回
function throwError(message: string): never {
  throw new Error(message);
}

// 死循环:永远不结束
function infiniteLoop(): never {
  while (true) {}
}

never 是最严格的类型:它可以赋值给任何类型,但除 never 之外没有任何类型能赋值给它。正因为如此,它能用来做穷尽性检查——这是它最有价值的用法:

type Shape = 'circle' | 'square';

function area(shape: Shape): number {
  switch (shape) {
    case 'circle':
      return 1;
    case 'square':
      return 2;
    default: {
      // 走到这里说明上面的分支没覆盖完
      // 如果给 Shape 新增成员却忘了加分支,这一行会编译报错
      const exhaustiveCheck: never = shape;
      throw new Error('未处理的形状: ' + exhaustiveCheck);
    }
  }
}

这里 shape 在 default 分支已被收窄为 never,所以赋值成立;一旦 Shape 增加新成员而 switch 没跟上,赋值就会失败并提示你补分支。另外,TS 里函数返回值推断为 never 还有一个推论:如果函数某个分支返回 never,那个分支不会影响返回类型的推断。

十、类型断言与类型收窄

declare const someValue: unknown;

// 尖括号语法:在 .tsx 里会和 JSX 冲突,不推荐
let strLength1: number = (<string>someValue).length;

// as 语法:推荐
let strLength2: number = (someValue as string).length;

// 实际场景:DOM 查询的返回类型需要收窄
const canvas = document.getElementById('main_canvas') as HTMLCanvasElement;

// 双重断言:转型跨度太大时,TS 会拦下来,需要中转一次
const value: unknown = 'hello';
const strLength3: number = (value as unknown as string).length;

as 不是万能的——只有当两个类型「有足够重叠」时才允许断言,否则会报 Conversion of type ... may be a mistake,必须用 as unknown as X 中转。断言是单方面告诉编译器「相信我」,它不会在运行时做任何转换,写错了照样崩。

所以优先用收窄(narrowing)而不是断言,编译器和运行时行为一致:

function format(input: string | number): string {
  if (typeof input === 'number') {
    return input.toFixed(2);        // 这个分支里 input 已收窄为 number
  }
  return input.trim();              // 这里收窄为 string
}

// 自定义类型守卫:返回类型写成 obj is User
interface User { id: number; name: string }

function isUser(obj: unknown): obj is User {
  return typeof obj === 'object' && obj !== null
    && typeof (obj as User).id === 'number'
    && typeof (obj as User).name === 'string';
}

十一、常见坑汇总

  1. 类型注解编译后全部消失:let n: number = 'a' as any 编译能过,运行时 n 就是字符串。类型系统只保护编译期。
  2. any 会传染:一处 any,后面整条调用链的类型检查都可能失效;开 noImplicitAny 兜底。
  3. 元组不是定长数组:push 不检查长度,运行时长度会变。要锁死就加 readonly。
  4. enum 不是零成本:会生成运行时代码;const enum 在 babel / esbuild 下需要额外配置。
  5. readonly T[] 与 T[] 不互相赋值:只读可以接收可变,反之不行。
  6. 数组下标越界不报错:类型是 T,实际可能是 undefined,开 noUncheckedIndexedAccess 才会提示。
  7. null / undefined 的行为取决于 strictNullChecks:没开的话空值检查基本失效。
  8. as 不做运行时转换:只是让编译器闭嘴,业务上仍要自己判空、判类型。
  9. 泛型箭头函数在 .tsx 里要写 <T,>:<T>(x: T) => x 会被当成 JSX 标签解析,加个逗号或 extends 即可。

十二、速查卡

类型 示例 典型用途
booleanlet ok: boolean = true开关、标志位
numberlet n: number = 0xf00d所有数字,无 int / float 之分
bigintlet b: bigint = 1n超过 2^53-1 的整数
stringlet s: string = 'a'文本,模板字符串也是它
symbolconst k = Symbol('k')唯一键
null / undefinedlet v: number | null = null用联合类型表示「可能没有值」
数组let a: number[] = [1] / Array<number>同类元素集合
元组let t: [string, number?]定结构数据、多返回值
enumenum C { Red, Green }常量集合(有运行时代价)
anylet x: any尽量不用
unknownlet x: unknown接外部数据,使用前须收窄
voidfunction f(): void无返回值函数
neverthrow / while(true)永不正常返回,穷尽性检查
objectlet o: object = {}非原始类型(注意它不表示「任意对象结构」)

最后提醒一句:object 表示「任何非原始类型」,它并不描述对象里有哪些属性——写 o.name 会直接报错。要描述结构请用 interface 或 type。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区