高级类型
交叉类型
是将多个类型合并为一个类型,包含了所需要的所有类型的特性
使用场景:大多是在混入(mixins)或其他不适合典型面向对象模型的地方看到交叉类型的使用
// 同时满足T跟U两种类型
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for(let id in first){
(<any>result)[id] = (<any>first)[id];
}
for(let id in second){
if(!result.hasOwnProperty(id)){
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
class Person {
constructor(public name: string){}
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
console.log('this is log')
}
}
var jim = extend(new Person('Jim'), new ConsoleLogger());
var n = jim.name;
jim.log();
联合类型
联合类型跟交叉类型很有关联,但使用上完全不同:
- 联合类型表示一个值可以是集中类型之一,用竖线(|)分割每个类型
- 如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员
// 这种方式padding传入的是any类型,也就是说可以传入number跟string以外的类型
function padLeft(value: string, padding: any){
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}'.`)
}
// 通过联合类型实现:控制只能传入两种指定类型
function padLeft1(value: string, padding: string | number){
// ...
}
interface Bird{
fly();
layEggs();
}
interface Fish{
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
// 类型保护与类型区分:上面例子通过这种方式判断会报错
let pet = getSmallPet();
// 每一个成员访问都会报错
if (pet.swim) {
pet.swim();
}
else if (pet.fly) {
pet.fly();
}
// 想判断类型需要使用类型断言
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
用户自定义的类型保护
pet is Fish 类型谓词:parameterName is Type 形式
// 每当使用一些变量调用isFish时,TS会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的
function isFish(pet: Fish | Bird): pet is Fish{
return (<Fish>pet).swim !== undefined;
}
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
}else {
pet.fly();
}
typeof 类型保护
function isNumber(x: any): x is number {
return typeof x === 'number';
}
function isString(x: any): x is string{
return typeof x === 'string';
}
function padL(value: string, padding: string | number) {
if(isNumber(padding)){
return Array(padding + 1).join(' ') + value;
}
if(isString(padding)){
return padding + value;
}
// 两种方式:typeof v === 'typename' 其中typename必须是:number,string,boolean,symbol
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}'.`);
}
instanceof 类型保护
是通过构造函数来细化类型的一种方式
// instanceof 右侧要求是一个构造函数,TS将其细化为:此构造函数的prototype属性的类型,如果他的类型不为any的话,构造签名所返回的类型的联合
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(' ');
}
let padder: Padder = getRandomPadder();
if(padder instanceof SpaceRepeatingPadder){
padder;
}
if(padder instanceof StringPadder){
padder;
}
类型别名
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型
- 类型别名也可以是泛型
- 也可以使用类型别名来在属性里引用自己
- 与交叉类型一起使用
注:起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型
注:类型别名不能出现在声明右侧的任何地方
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
}
else {
return n();
}
}
// 泛型
type Container<T> = { value: T };
// 用类型别名来在属性里引用自己
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
// 与交叉类型一起使用,可以创建出一些十分稀奇古怪的类型
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
name: string;
}
var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
// 类型别名不能出现在声明右侧的任何地方
type Yikes = Array<Yikes>; // error
接口 vs. 类型别名
类型别名可以像接口一样,但仍然有一些区别:
- 接口创建了名字,可以在其他任何地方使用,类型并不创建新名字:如,错误信息就不会使用别名
- 类型不能被extends和implements(自己也不能extends和implements其他类型)
注:因为软件中对象应该对于扩展是开放的,但对于修改是封闭的,应该尽量使用接口代替类型别名
type Alias = { num: number };
interface Interface { num: number };
// 在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface
declare function aliased(arg: Alias): Alias;
// 悬停在 aliased上时,显示的却是对象字面量类型
declare function interfaced(arg: Interface): Interface;
字符串字面量类型
允许指定字符串必须的固定值
注:在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合,通过结合使用这些特性,可以实现类似枚举类型的字符串
数字字面量类型:很少使用可以缩小范围调试bug
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing){
if(easing === "ease-in"){
console.log('this is ease-in');
}else if (easing === "ease-out"){
console.log('this is ease-out');
}else if (easing === "ease-in-out"){
console.log('this is ease-in-out');
}else {
console.log('no animate');
}
}
}
let button = new UIElement();
button.animate(0,0,'ease-in');
button.animate(0, 0, "uneasy"); // error no animate
// 数字字面量类型
function rollDie(x: number) : 1 | 2 | 3 | 4 {
// 当 x与 2进行比较的时候,它的值必须为 1,这就意味着下面的比较检查是非法的
if(x !== 1 || x !== 2) {}
return 1;
}
可辨识联合
可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做可辨识联合的高级模式,也称作:标签联合或代数数据类型
TS基于已有的JS模式有三个要素:
- 具有普通的单例类型属性---可辨识的特征
- 一个类型别名包含了那些类型的联合---联合
- 此属性上的类型保护
// 声明了联合的接口,每个接口都有kind属性但有不同的字符串字面量类型
// kind属性:称作可辨识的特征或标签,其他属性则特定于各自接口
// 注:目前各个接口时没有联系的
interface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
interface Circle {
kind: 'circle';
radius: number;
}
// 把接口联合到一起
type Shape = Square | Rectangle | Circle;
// 使用可辨识联合
function area(s: Shape) {
switch(s.kind){
case 'square': return s.size * s.size;
case 'rectangle': return s.height * s.width;
case 'circle': return Math.PI * s.radius ** 2;
}
}
完整性检查
当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们
比如,如果添加了Triangle到 Shape,同时还需要更新area
有两种方式可以实现:
- 首先是启用 --strictNullChecks并且指定一个返回值类型
- 使用never类型,编译器用它来进行完整性检查
// 因为 switch没有包涵所有情况,所以TypeScript认为这个函数有时候会返回 undefined。
// 如果你明确地指定了返回值类型为 number,那么你会看到一个错误,因为实际上返回值的类型为 number | undefined
function area1(s: Shape): number {
switch(s.kind){
case 'square': return s.size * s.size;
case 'rectangle': return s.height * s.width;
case 'circle': return Math.PI * s.radius ** 2;
}
}
// 第二种方法使用 never类型
function assertNever(x: never): never{
throw new Error("Unexpected object: " + x);
}
// assertNever检查 s是否为 never类型—即为除去所有可能情况后剩下的类型
// 如果忘记了某个case,那么 s将具有一个真实的类型并且你会得到一个错误
function area2(s: Shape): number {
switch(s.kind){
case 'square': return s.size * s.size;
case 'rectangle': return s.height * s.width;
case 'circle': return Math.PI * s.radius ** 2;
default: return assertNever(s);
}
}
多态的this类型
表示某个包含类或接口的子类型,这被称作 F-bounded多态性
应用:他能很容易的表现连贯接口间的继承
class BasicCalculator {
public constructor(protected value: number = 0){}
public currentValue(): number {
return this.value;
}
public add(operand: number): this {
this.value == operand;
return this;
}
public multiply(operand: number): this {
this.value *= operand;
return this;
}
}
let val = new BasicCalculator(2).multiply(5).add(1).currentValue();
// 由于这个类使用了 this类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变
class ScientificCalculator extends BasicCalculator {
public constructor(value = 0){
super(value);
}
public sin(){
this.value = Math.sin(this.value);
return this;
}
}
let scientVal = new ScientificCalculator(2).multiply(5).sin().add(1).currentValue();
索引类型
使用索引类型,编译器就能够检查使用了动态属性名的代码
keyof T:索引类型查询操作符,对于任何类型T,keyof T的结果为T上已知的公共属性名的联合
T[K]:索引访问操作符
索引类型和字符串索引签名:keyof和 T[K]与字符串索引签名进行交互
注: 如果你有一个带有字符串索引签名的类型,那么 keyof T会是 string。 并且 T[string]为索引签名的类型
// JS模式从对象中选取属性的子集
function pluck(o, names){
return names.map(n => o[n]);
}
// 在TS中通过索引类型检查和索引访问操作符
function pluck1<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 20
}
let strings: string[] = pluck1(person, ['name']);
// 索引类型和字符串索引签名
interface Maps<T> {
[key: string]: T;
}
let keys: keyof Maps<number>; // string
let value: Maps<number>['foo']; // number
console.log('keys: ' + keys, + ' value: ' + value)
映射类型
TS提供了从旧类型中创建新类型的一种方式 — 映射类型
定义:在映射类型里,新类型以相同的形式去转换旧类型里每个属性
语法与索引签名的语法类型,内部使用了for...in具有三个部分:
- 类型变量K,他会依次绑定到每个属性
- 字符串字面量联合的keys,包含了要迭代的属性名的集合
- 属性的结果类型
// 一个常见的任务是将一个已知的类型每个属性都变为可选的:
interface PersonPartial1 {
name?: string;
age?: number;
}
// 或者只想要一个只读版本:
interface PersonReadonly1 {
readonly name: string;
readonly age: number;
}
// 映射类型
type Readonlyp<T> = {
readonly [P in keyof T]: T[P];
}
type Partials<T> = {
[P in keyof T]?: T[P];
}
// 使用
type PersonPartial = Partials<Person>;
type ReadonlyPerson = Readonlyp<Person>;
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
预定义的有条件类型
TS2.8中增加了一些预定义的有条件类型:
- Exclude<T, U> -- 从T中剔除可以赋值给U的类型
- Extract<T, U> -- 提取T中可以赋值给U的类型
- NonNullable<T> -- 从T中剔除null和undefined
- ReturnType<T> -- 获取函数返回值类型
- InstanceType<T> -- 获取构造函数类型的实例类型
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
type T02 = Exclude<string | number | (() => void), Function>; // string | number
type T03 = Extract<string | number | (() => void), Function>; // () => void
type T04 = NonNullable<string | number | undefined>; // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[]
function f1(s: string) {
return { a: 1, b: s };
}
class C {
x = 0;
y = 0;
}
type T10 = ReturnType<() => string>; // string
type T11 = ReturnType<(s: string) => void>; // void
type T12 = ReturnType<(<T>() => T)>; // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[]
type T14 = ReturnType<typeof f1>; // { a: number, b: string }
type T15 = ReturnType<any>; // any
type T16 = ReturnType<never>; // any
// type T17 = ReturnType<string>; // Error
// type T18 = ReturnType<Function>; // Error
type T20 = InstanceType<typeof C>; // C
type T21 = InstanceType<any>; // any
type T22 = InstanceType<never>; // any
// type T23 = InstanceType<string>; // Error
// type T24 = InstanceType<Function>; // Error