.d.ts 文件只包含接口,不包含实现。npm 包安装后一般包含.d.ts文件和编译后的js文件,这里的js文件一般是bundled (多个源文件合并成一个主文件)

The declare keyword: It only declares it for the TypeScript compiler, not for the JavaScript runtime.

Basic Types

Array types can be written in one of two ways. They are Arrays in JS with elements of the same type.

  1. let list: number[] = [1, 2, 3];
  2. let list: Array<number> = [1, 2, 3];

Tuples are the Arrays of fixed number of elements (of known types) in JS

  1. // Declare a tuple type
  2. let x: [string, number];

TS has an extra enumeration type.

  1. enum Color {
  2. Red = 1,
  3. Green = 2,
  4. Blue = 4,
  5. }
  6. let c: Color = Color.Green;
  7. let colorName: string = Color[4]; // 'Blue'

unknown 类型是有type check的

  1. declare const maybe: unknown;
  2. // 'maybe' could be a string, object, boolean, undefined, or other types
  3. const aNumber: number = maybe;
  4. // Type 'unknown' is not assignable to type 'number'.
  5. if (maybe === true) {
  6. // TypeScript knows that maybe is a boolean now
  7. const aBoolean: boolean = maybe;
  8. // So, it cannot be a string
  9. const aString: string = maybe;
  10. // Type 'boolean' is not assignable to type 'string'.
  11. }

If you have a variable with an unknown type, you can narrow it to something more specific by doing typeof checks, comparison checks, or more advanced type guards that will be discussed.

any: opt-out type checking, it may propagate

  1. declare function getValue(key: string): any;
  2. // OK, return value of 'getValue' is not checked
  3. const str: string = getValue("myString");
  4. let looselyTyped: any = {};
  5. let d = looselyTyped.a.b.c.d;
  6. // ^ = let d: any

Remember that all the convenience of any comes at the cost of losing type safety. Type safety is one of the main motivations for using TypeScript and you should try to avoid using any when not necessary.

void 主要用于函数返回;用在变量上没什么意义

By default null and undefined are subtypes of all other types. That means you can assign null and undefined to something like number.

However, when using the --strictNullChecks flag, null and undefined are only assignable to unknown, any and their respective types (the one exception being that undefined is also assignable to void). This helps avoid many common errors. In cases where you want to pass in either a string or null or undefined, you can use the union type string | null | undefined.

The never type represents the type of values that never occur. For instance, never is the return type for a function expression or an arrow function expression that always throws an exception or one that never returns. Variables also acquire the type never when narrowed by any type guards that can never be true.

  1. // Inferred return type is never
  2. function fail() {
  3. return error("Something failed");
  4. }
  5. // Function returning never must not have a reachable end point
  6. function infiniteLoop(): never {
  7. while (true) {}
  8. }

The never type is a subtype of, and assignable to, every type; however, no type is a subtype of, or assignable to, never (except never itself). Even any isn’t assignable to never.

object

  1. declare function create(o: object | null): void;

Type assertions are a way to tell the compiler “trust me, I know what I’m doing.” A type assertion is like a type cast in other languages, but it performs no special checking or restructuring of data. It has no runtime impact and is used purely by the compiler. TypeScript assumes that you, the programmer, have performed any special checks that you need.

Type assertions have two forms.
One is the as-syntax:

  1. let someValue: unknown = "this is a string";
  2. let strLength: number = (someValue as string).length;

The other version is the “angle-bracket” syntax:

  1. let someValue: unknown = "this is a string";
  2. let strLength: number = (<string>someValue).length;

The two samples are equivalent. Using one over the other is mostly a choice of preference; however, when using TypeScript with JSX, only as-style assertions are allowed.

Both version cannot be used in destructuring assignment:

  1. interface SlateNode{}
  2. interface ListNode extends SlateNode{
  3. type: string
  4. }
  5. type Path = number[]
  6. type NodeEntry = [SlateNode, Path]
  7. let entry: NodeEntry = [{ type: 'ul' } as SlateNode, [0,1]]
  8. const [(node as ListNode)] = entry // error
  9. console.log(node.type) // error

Possible solutions

  1. const [node] = entry as [ListNode, Path] // ok
  2. //or
  3. console.log((node as ListNode).type) //ok

Interfaces

Interfaces are used to describe specific object types, including functions.

Structural SubTyping

optional properties
readonly: ReadonlyArray
readonly vs const
The easiest way to remember whether to use readonly or const is to ask whether you’re using it on a variable or a property. Variables use const whereas properties use readonly.

Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the “target type” doesn’t have, you’ll get an error:

  1. function createSquare(config: SquareConfig): { color: string; area: number } {
  2. return { color: config.color || "red", area: config.width ? config.width*config.width : 20 };
  3. }
  4. let mySquare = createSquare({ colour: "red", width: 100 });

Getting around these checks is actually really simple. The easiest method is to just use a type assertion:

  1. let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

However, a better approach might be to add a string index signature if you’re sure that the object can have some extra properties that are used in some special way. If SquareConfig can have color and width properties with the above types, but could also have any number of other properties, then we could define it like so:

  1. interface SquareConfig {
  2. color?: string;
  3. width?: number;
  4. [propName: string]: any;
  5. }

We’ll discuss index signatures in a bit, but here we’re saying a SquareConfig can have any number of properties, and as long as they aren’t color or width, their types don’t matter.
One final way to get around these checks, which might be a bit surprising, is to assign the object to another variable: Since squareOptions won’t undergo excess property checks, the compiler won’t give you an error.

  1. let squareOptions = { colour: "red", width: 100 };
  2. let mySquare = createSquare(squareOptions);

The above workaround will work as long as you have a common property between squareOptionsand SquareConfig. In this example, it was the property width. It will however, fail if the variable does not have any common object property.

Function Types

To describe a function type with an interface, we give the interface a call signature. This is like a function declaration with only the parameter list and return type given. Each parameter in the parameter list requires both name and type.

  1. interface SearchFunc {
  2. (source: string, subString: string): boolean;
  3. }
  4. let mySearch: SearchFunc;
  5. mySearch = function (src: string, sub: string): boolean {
  6. // the names of the parameters do not need to match
  7. let result = src.search(sub);
  8. return result > -1;
  9. };
  10. // types can be inferred
  11. let mySearch2: SearchFunc;
  12. mySearch2 = function (src, sub) {
  13. let result = src.search(sub);
  14. return result > -1;
  15. };

Indexable Types

Indexable types have an index signature that describes the types we can use to index into the object, along with the corresponding return types when indexing.

There are two types of supported index signatures: string and number. It is possible to support both types of indexers, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing with a number, JavaScript will actually convert that to a string before indexing into an object. That means that indexing with 100 (a number) is the same thing as indexing with "100" (a string), so the two need to be consistent.

While string index signatures are a powerful way to describe the “dictionary” pattern, they also enforce that all properties match their return type.

  1. interface NumberDictionary {
  2. [index: string]: number;
  3. length: number; // ok, length is a number
  4. name: string; // error, the type of 'name' is not a subtype of the indexer
  5. }

Class Types

When working with classes and interfaces, it helps to keep in mind that a class has two types: the type of the static side and the type of the instance side.

[Todo]

Extending Interfaces

An interface can extend multiple interfaces, creating a combination of all of the interfaces.

Hybrid Types

function object owning other attributes

  1. interface Counter {
  2. (start: number): string;
  3. interval: number;
  4. reset(): void;
  5. }
  6. function getCounter(): Counter {
  7. let counter = function (start: number) {} as Counter;
  8. counter.interval = 123;
  9. counter.reset = function () {};
  10. return counter;
  11. }
  12. let c = getCounter();
  13. c(10);
  14. c.reset();
  15. c.interval = 5.0;

Interfaces Extending Classes

When an interface type extends a class type it inherits the members of the class but not their implementations. It is as if the interface had declared all of the members of the class without providing an implementation. Interfaces inherit even the private and protected members of a base class. This means that when you create an interface that extends a class with private or protected members, that interface type can only be implemented by that class or a subclass of it.

  1. class Control {
  2. private state: any;
  3. }
  4. interface SelectableControl extends Control {
  5. select(): void;
  6. }
  7. class Button extends Control implements SelectableControl {
  8. select() {}
  9. }
  10. class TextBox extends Control {
  11. select() {}
  12. }
  13. class ImageControl implements SelectableControl {
  14. Class 'ImageControl' incorrectly implements interface 'SelectableControl'.
  15. Types have separate declarations of a private property 'state'.
  16. private state: any;
  17. select() {}
  18. }

Functions

The return type can often be inferred

  1. function foo(name: string, height: number){
  2. if (height > 0){
  3. return height
  4. }else{
  5. return name
  6. }
  7. }
  8. let h:number = foo('gy', 100) // Type 'string | number' is not assignable to type 'number'

When writing out the whole function type, both parts are required. If the function doesn’t return a value, you would use void instead of leaving it off.

  1. let myAdd: (baseValue: number, increment: number) => number = function (
  2. x: number,
  3. y: number
  4. ): number {
  5. return x + y;
  6. };

Of note, only the parameters and the return type make up the function type. Captured variables are not reflected in the type. In effect, captured variables are part of the “hidden state” of any function and do not make up its API.

Funciton types can be inferred:

  1. // The parameters 'x' and 'y' have the type number
  2. let myAdd = function (x: number, y: number): number {
  3. return x + y;
  4. };
  5. // myAdd has the full function type
  6. let myAdd2: (baseValue: number, increment: number) => number = function (x, y) {
  7. return x + y;
  8. };

This is called “contextual typing”, a form of type inference.

The number of arguments given to a function has to match the number of parameters the function expects. In JavaScript, every parameter is optional, and users may leave them off as they see fit. When they do, their value is undefined. We can get this functionality in TypeScript by adding a ? to the end of parameters we want to be optional.

Default-initialized parameters that come after all required parameters are treated as optional, and just like optional parameters, can be omitted when calling their respective function.

  1. function buildName(firstName: string, lastName?: string) {
  2. // ...
  3. }
  4. function buildName(firstName: string, lastName = "Smith") {
  5. // ...
  6. }

Both share the same type (firstName: string, lastName?: string) => string
If a default-initialized parameter comes before a required parameter, users need to explicitly pass undefined to get the default initialized value.

Required, optional, and default parameters all have one thing in common: they talk about one parameter at a time. Sometimes, you want to work with multiple parameters as a group, or you may not know how many parameters a function will ultimately take. In JavaScript, you can work with the arguments directly using the arguments variable that is visible inside every function body.

In typescript, you can use rest parameters

  1. function buildName(firstName: string, ...restOfName: string[]) {
  2. return firstName + " " + restOfName.join(" ");
  3. }
  4. // employeeName will be "Joseph Samuel Lucas MacKinzie"
  5. let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
  1. let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

this

Yehuda Katz’s Understanding JavaScript Function Invocation and “this”explains the inner workings of this very well

Arrow functions capture the this where the function is created rather than where it is invoked.
Even better, TypeScript will warn you when you make this mistake if you pass the --noImplicitThis flag to the compiler.

  1. let deck = {
  2. suits: ["hearts", "spades", "clubs", "diamonds"],
  3. cards: Array(52),
  4. createCardPicker: function () {
  5. // NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
  6. return () => {
  7. let pickedCard = Math.floor(Math.random() * 52);
  8. let pickedSuit = Math.floor(pickedCard / 13);
  9. return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
  10. };
  11. },
  12. };
  13. let cardPicker = deck.createCardPicker();
  14. let pickedCard = cardPicker();
  15. alert("card: " + pickedCard.card + " of " + pickedCard.suit);

Unfortunately, the type of this.suits[pickedSuit] is still any.
To fix this, you can provide an explicit this parameter. this parameters are fake parameters that come first in the parameter list of a function:

  1. interface Card {
  2. suit: string;
  3. card: number;
  4. }
  5. interface Deck {
  6. suits: string[];
  7. cards: number[];
  8. createCardPicker(this: Deck): () => Card;
  9. }
  10. let deck: Deck = {
  11. suits: ["hearts", "spades", "clubs", "diamonds"],
  12. cards: Array(52),
  13. // NOTE: The function now explicitly specifies that its callee must be of type Deck
  14. createCardPicker: function (this: Deck) {
  15. return () => {
  16. let pickedCard = Math.floor(Math.random() * 52);
  17. let pickedSuit = Math.floor(pickedCard / 13);
  18. return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
  19. };
  20. },
  21. };
  22. let cardPicker = deck.createCardPicker();
  23. let pickedCard = cardPicker();
  24. alert("card: " + pickedCard.card + " of " + pickedCard.suit);

You can also run into errors with this parameter in callbacks. To solve it, first, the library author needs to annotate the callback type with this

  1. interface UIElement {
  2. addClickListener(onclick: (this: void, e: Event) => void): void;
  3. }

this: void means that addClickListener expects onclick to be a function that does not require a this type. Second, annotate your calling code with this:

  1. class Handler {
  2. info: string;
  3. onClickGood(this: void, e: Event) {
  4. // can't use `this` here because it's of type void!
  5. console.log("clicked!");
  6. }
  7. }
  8. let h = new Handler();
  9. uiElement.addClickListener(h.onClickGood);

Of course, this also means that it can’t use this.info. If you want both then you’ll have to use an arrow function:

  1. class Handler {
  2. info: string;
  3. onClickGood = (e: Event) => {
  4. this.info = e.message;
  5. };
  6. }

This works because arrow functions use the outer this, so you can always pass them to something that expects this: void . The downside is that one arrow function is created per object of type Handler. Methods, on the other hand, are only created once and attached to Handler’s prototype. They are shared between all objects of type Handler.

overloads

It’s not uncommon for a single JavaScript function to return different types of objects based on the shape of the arguments passed in. How do we describe this to the type system?

The answer is to supply multiple function types for the same function as a list of overloads.

  1. function pickCard(x: { suit: string; card: number }[]): number;
  2. function pickCard(x: number): { suit: string; card: number };
  3. function pickCard(x: any): any {
  4. //
  5. }

In order for the compiler to pick the correct type check, it follows a similar process to the underlying JavaScript. It looks at the overload list and, proceeding with the first overload, attempts to call the function with the provided parameters. If it finds a match, it picks this overload as the correct overload. For this reason, it’s customary to order overloads from most specific to least specific.

Note that the function pickCard(x): any piece is not part of the overload list, so it only has two overloads: one that takes an object and one that takes a number. Calling pickCard with any other parameter types would cause an error.

Literal Types

There are three sets of literal types available in TypeScript today: strings, numbers, and booleans; They can be mixed.

  1. type t = 'hi' | 2 | false

Literal narrowing

  1. // We're making a guarantee that this variable
  2. // helloWorld will never change, by using const.
  3. // So, TypeScript sets the type to be "Hello World" not string
  4. const helloWorld = "Hello World";
  5. // On the other hand, a let can change, and so the compiler declares it a string
  6. let hiWorld = "Hi World";

The process of going from an infinite number of potential cases (there are an infinite number of possible string values) to a smaller, finite number of potential case is called narrowing.

Literal types can be used in the same way to distinguish overloads:

  1. function createElement(tagName: "img"): HTMLImageElement;
  2. function createElement(tagName: "input"): HTMLInputElement;
  3. // ... more overloads ...
  4. function createElement(tagName: string): Element {
  5. // ... code goes here ...
  6. }

Unions and Intersection Types

A union type describes a value that can be one of several types.

Unions with Common Fields

If a value has the type A | B, we only know for certain that it has members that both A and B have. (no matter whether they have common fields.)

  1. interface Bird {
  2. fly(): void;
  3. layEggs(): void;
  4. }
  5. interface Fish {
  6. swim(): void;
  7. layEggs(): void;
  8. }
  9. declare function getSmallPet(): Fish | Bird;
  10. let pet = getSmallPet();
  11. pet.layEggs();
  12. // error, only available in one of the two possible types
  13. pet.swim();

Discriminating Unions

A common technique for working with unions is to have a single field which uses literal types which you can use to let TypeScript narrow down the possible current type. (Typescript will narrow it automatically)

  1. type NetworkLoadingState = {
  2. state: "loading";
  3. };
  4. type NetworkFailedState = {
  5. state: "failed";
  6. code: number;
  7. };
  8. type NetworkSuccessState = {
  9. state: "success";
  10. response: {
  11. title: string;
  12. duration: number;
  13. summary: string;
  14. };
  15. };
  16. // Create a type which represents only one of the above types
  17. // but you aren't sure which it is yet.
  18. type NetworkState =
  19. | NetworkLoadingState
  20. | NetworkFailedState
  21. | NetworkSuccessState;
  22. function logger(state: NetworkState): string {
  23. // By switching on state, TypeScript can narrow the union
  24. state.code //error
  25. // down in code flow analysis
  26. switch (state.state) {
  27. case "loading":
  28. return "Downloading...";
  29. case "failed":
  30. // The type must be NetworkFailedState here,
  31. // so accessing the `code` field is safe
  32. return `Error ${state.code} downloading`;
  33. case "success":
  34. return `Downloaded ${state.response.title} - ${state.response.summary}`;
  35. }
  36. }

Intersection Types

An intersection type combines multiple types into one. This allows you to add together existing types to get a single type that has all the features you need.

  1. interface ErrorHandling {
  2. success: boolean;
  3. error?: { message: string };
  4. }
  5. interface ArtworksData {
  6. artworks: { title: string }[];
  7. }
  8. interface ArtistsData {
  9. artists: { name: string }[];
  10. }
  11. // These interfaces are composed to have
  12. // consistent error handling, and their own data.
  13. type ArtworksResponse = ArtworksData & ErrorHandling;
  14. type ArtistsResponse = ArtistsData & ErrorHandling;
  15. const handleArtistsResponse = (response: ArtistsResponse) => {
  16. if (response.error) {
  17. console.error(response.error.message);
  18. return;
  19. }
  20. console.log(response.artists);
  21. };

union and intersection support one operand:

  1. type A = | string // i.e. string
  2. type B = & number // i.e. number
  3. type C =
  4. | string
  5. | number