A strongly typed programming language that builds on JavaScript.
Why typescript ?
Catch type errors beforehand
TypeScript adds additional syntax to JavaScript to catch type errors - the most common errors for programmers.
Example
Run and compile typescript
Run typescript
ts-node: Typescript execution and REPL for node.js, with source map and native ESM support.
run a file:
script.ts:
1 2 3 4 5 6
const x = 10 const y = 'foo' functionadd(x: number, y: number) { return x + y } console.log(add(x, y))
ts-node script.ts gives:
1 2 3 4 5
> index.ts:859 return new TSError(diagnosticText, diagnosticCodes, diagnostics); ^ TSError: ⨯ Unable to compile TypeScript: test.ts:6:20 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
REPL example:
1 2 3 4 5 6 7 8 9
> ts-node > const x = 10 undefined > const y = 'foo' undefined > function add(x: number, y: number) { return x + y } undefined > add(x, y) error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'
// Primitive types: letisDone: boolean = false letdecimal: number = 6 letmyName: string = 'Tom' // void can only be assigned undefined or null letuseless1: void = undefined letuseless2: void = null // null and undefined can also be used to define types // they can be assigned interchangeably letuseless3: undefined = null letuseless4: null = undefined // difference between null/undefined and void is that any type can assign its value to null/undefined but not void // the following is ok: letnum1: number = undefined letu1: undefined letnum2: number = u1 // but this is not ok: ts2322 - Type void is not assignable to type number letu2: void = undefined letnum3: number = u2
// Any type: // normal types cannot change its type: letmyFavoriteNumber1: string = 'seven'; myFavoriteNumber1 = 7; // ts2322 - Type number is not assignable to type string // any type can be assgined to another type letmyFavoriteNumber2: any = 'seven' myFavoriteNumber2 = 7; // if a variable is not assigned to a type, it will implicitly be assigned to any type let something // ts-7043: Variable 'something' implicitly has an 'any' type, but a better type may be inferred from usage. something = 'seven' something = 7 something.setName('Tom')
Type inference
1 2 3 4 5 6 7 8 9 10 11 12 13
let myFavoriteNumber = 'seven'; myFavoriteNumber = 7; // Type 'number' is not assignable to type 'string'.ts(2322)
// Above code is equivalent to: letmyFavoriteNumber: string = 'seven'; myFavoriteNumber = 7; // Typescript tries to infer a type when no type is specified
// If not initialized, variable will be inferred as any type let myFavoriteNumber myFavoriteNumber = 'seven' myFavoriteNumber = 7
letmyFavoriteNumber: string | number; myFavoriteNumber = 'seven'; myFavoriteNumber = 7; // above is ok
myFavoriteNumber = true; // error TS2322: Type 'boolean' is not assignable to type 'string | number'.
functiongetLength(something: string | number): number { return something.length; } // error TS2339: Property 'length' does not exist on type 'string | number'. // Property 'length' does not exist on type 'number'. // Only property owned by all types in a union type can be accessed.
letmyFavoriteNumber: string | number myFavoriteNumber = 'seven' console.log(myFavoriteNumber.length) // This works well myFavoriteNumber = 7 console.log(myFavoriteNumber.length) // This will throw an error: // error TS2339: Property 'length' does not exist on type 'number'. // Type of a union type will be inferred after assigning a value.
letjack: Person = { name: 'Jack' } // Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.ts(2741) // test.ts(3, 3): 'age' is declared here. // variable declared must have all properties assigned
letjohn: Person = { name: 'John', age: 31, sex: 'male' } // Type '{ name: string; age: number; sex: string; }' is not assignable to type 'Person'. // Object literal may only specify known properties, and 'sex' does not exist in type 'Person'.ts(2322) // variable also cannot have additional properties
interfaceHuman { name: string, age?: number } // Question mark in a interface property means it's optional letjane: Human = { name: 'Jane' } // jane is ok letjoe: Human = { name: 'Joe', sex: 'female' } // Type '{ name: string; sex: string; }' is not assignable to type 'Human'. // Object literal may only specify known properties, and 'sex' does not exist in type 'Human'.ts(2322) // still no additional property allowed
interfacePerson { name: string, age?: number, [propName: string]: string } // [propName: string]: any means any whatever property name is allowed with string value lettom: Person = { name: 'Tom', sex: 'male', // this property is ok birthdate: newDate() // this won't work: // Type 'Date' is not assignable to type 'string'.ts(2322) // test.ts(4, 3): The expected type comes from this index signature. }
// Union types can also be used interfaceHuman { name: string; age?: number; [propName: string]: string | number; }
// Interface can also be used to represent an array: interfaceNumberArray { [index: number]: number; } letfibonacci: NumberArray = [1, 1, 2, 3, 5]; // but it's so complex, rarely used except in Array-like object
functionsum(a: number, b: number) { letargs: number = arguments } // error: Type 'IArguments' is not assignable to type 'number'.ts(2322) // arguments is an object array
// Declare a function let mySum = function(x: number, y: number): number { return x + y }
// Declare a function with type predefined // In Typescript, => is used to define the shape of a function // left of => are argument types // right of => is return value type letmySum: (x: number, y: number) =>number = function (x: number, y: number): number { return x + y; };
// Shape a function with interface: interfaceSearchFunc { (source: string, subString: string): boolean; }
// Use ? to mark an optional parameter // Note optional parameters must be after required parameters // No required parameter is allowed after optional parameter const fullName = (first: string, last: string, middle?: string): string { if (middle) { return first + ' ' + middle + ' ' + last } else { return first + ' ' + last } } fullName('陈', '洞天') fullName('陈', '洞', '天')
// Parameter default value functionbuildName(firstName: string, lastName: string = 'Cat') { return firstName + ' ' + lastName; } let tomcat = buildName('Tom', 'Cat'); let tom = buildName('Tom');
// To reverse a number or string: functionreverseNS(x: number | string): number | string | void { if (typeof x === 'number') { returnNumber(x.toString().split('').reverse().join('')); } elseif (typeof x === 'string') { return x.split('').reverse().join(''); } } // However it's not obvious that // reverseNS returns a number when input is number // and returns a string when input is string. // Rewrite it with function overload: functionreverse(x: number): number; functionreverse(x: string): string; functionreverse(x: number | string): number | string | void { if (typeof x === 'number') { returnNumber(x.toString().split('').reverse().join('')); } elseif (typeof x === 'string') { return x.split('').reverse().join(''); } } console.log(reverse(123)) // It works and editor gives 'right' hint. // It calls reverse(x: number | string): number | string | void // but editor hints reverse(x: number): number
// Declare a class through class declaration: classRectangle { constructor(height, width) { this.height = height; this.width = width; } }
// Declare a class through class expression: letRectangle = class { constructor(height, width) { this.height = height; this.width = width; } }
console.log(Rectangle.name); // output: Rectangle
classRectangle { // constructor is a special method for creating and initializing an object created with a class constructor(height, width) { this.height = height; this.width = width; } // Getter. Getter method is used like a normal property. getarea() { returnthis.calcArea(); }
// static members area called without instantiating their class and **cannot** be called through a class instance. // static methods are often used to create utility functions for an application // static properties are useful for caches, fixed-configuration, or other replicated data static shortName = 'Rect' staticareaDiff(rect1, rect2) { return rect1.area - rect2.area }
console.log((newDateNew()).getAbbrMonth()); // expected output: "Aug" (current time 2022.8)
Typescript class
Typescript class is Javascript class, with a BIT MORE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
letRectangle = class { statictype: string = 'SimpleShape'// Typescript supports static property but javascript doesn't public height: number // All properties must be defined public width: number // All properties must be defined private secret: string = 'Secret'// Private property is only accessible inside the class protected meOrChild: string = 'meOrChild'// Protected property is only accessible inside the class and its child class
let rect = newRectangle(10, 10) console.log(rect.secret) // Typescript will throw error but COMPILED JAVASCRIPT WON'T // ONLY EDITOR WILL TYPECHECK // JAVASCRIPT HAS NO WAY TO LIMIT PROPERTY ACCESS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
classAnimal { public name; privateconstructor(name) { this.name = name; } } classCatextendsAnimal { constructor(name) { super(name); } } // Cannot extend a class with a private constructor: // Cannot extend a class 'Animal'. Class constructor is marked as private.ts(2675)
let a = newAnimal('Jack'); // Cannot instantiate a class with a private constructor: // Constructor of class 'Animal' is private and only accessible within the class declaration.ts(2673)
let a = newAnimal('Jack'); // A class with protected constructor can only be inherit/extended // index.ts(13,9): TS2674: Constructor of class 'Animal' is protected and only accessible within the class declaration.
classCatextendsAnimal { publiceat() { console.log(`${this.name} is eating.`); } }
let cat = newCat('Tom'); // abstract method in abstract class must be implemented // index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.
interfacePoint3d extendsPoint { z: number; } // An interface can extend a class! letpoint3d: Point3d = {x: 1, y: 2, z: 3}; // A class in Typescript is also a type, // For the Point class above, it also serves like a type: typePointType = { x: number, y: number } // That's why class in Typescript can be extended by an interface. // Note that when consider class as a type, its constructor and static // properties/methods are excluded.
functionisFish(animal: Cat | Fish) { if (typeof animal.swim === 'function') { returntrue; } returnfalse; } // error TS2339: Property 'swim' does not exist on type 'Cat | Fish'. // Property 'swim' does not exist on type 'Cat'. // For union types, only property they have in common can be accessed
// To solve this, use type assertion: functionisFish(animal: Cat | Fish) { if (typeof (animal asFish).swim === 'function') { returntrue; } returnfalse; } // type assertion Syntax: VALUE as TYPE
functionswim(animal: Cat | Fish) { (animal asFish).swim(); } consttom: Cat = { name: 'Tom', run() { console.log('run') } }; swim(tom); // Above code is ok at compiling, but throws error at running: // Uncaught TypeError: animal.swim is not a function` // Type assertion may 'hide' errors, so use it with caution // Only use type assertion when you are fully sure that the type is correct after assertion
functionisApiError(error: Error) { if (error instanceofApiError) { returntrue; } returnfalse; } // Editor check error: 'ApiError' only refers to a type, but is being used as a value here.ts(2693) // cannot use instanceof on an Typescript interface, instanceof cannot be used on Javascript class
// To solve above problem, use type assertion: interfaceApiErrorextendsError { code: number; } interfaceHttpErrorextendsError { statusCode: number; }
// Above code works well, no error is thrown. // Although Cat and Animal are two distinct interfaces and no extends between them // under the hood Typescript thinks Cat is descendant of Animal. // Typescript only compares their shapes, so it is identical to Cat extends Animal. // When two interfaces are COMPATIBLE, they can be asserted to each other. // Father can be asserted to son and vice versa.
// A conclusion to type assertion
// 1. Union types can be asserted to one of them // 2. Father/Son classes can be asserted to each other // All types can be asserted to any and vice verse // Compatible types can be asserted toeach other
// Type assertion only affects compilation functiontoBoolean(something: any): boolean { return something asboolean; }
toBoolean(1); // Above code will be compiled to functiontoBoolean(something) { return something; }
toBoolean(1); // nothing is done on the return value. // To transform a type, use type transformation: functiontoBoolean(something: any): boolean { returnBoolean(something); // Transforms to boolean }
toBoolean(1);
Type alias
Type alias gives a type an alias.
1 2
type str = string letb:str = 'abc'
type keyword can also type an enum.
1 2 3
typeNum123 = 1 | 2 | 3 typeCharABC = 'a' | 'b' | 'c' letc:Num123 = 4// Error Type '4' is not assignable to type 'Num123'.ts(2322)
// Manually assign value to enum enumDays {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 7); // true console.log(Days["Mon"] === 1); // true console.log(Days["Tue"] === 2); // true console.log(Days["Sat"] === 6); // true // enum not assigned manually will be assigned a value // one greater than its previous value.
1 2 3 4 5 6
// wednes will override sun as 3 enumDays {sun = 3, mon = 1, tues, wednes, thurs, fri, satur}
What if we want to create an array of any type with length 3 ?
1 2 3 4 5 6 7 8 9 10
functioncreateArray(length: number, value: any): Array<any> { let result = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; }
createArray(3, 'x'); // ['x', 'x', 'x'] // It works but the array generated does not have a specific type.
Here’s the solution using generics:
1 2 3 4 5 6 7 8 9 10
function createArray<T>(length: number, value: T): Array<T> { letresult: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; }
createArray<string>(3, 'x') // ['x', 'x', 'x'] createArray(3, 'x') // this also works using type inference
1 2 3 4 5 6 7 8 9 10
interfaceLengthwise { length: number; }
function loggingIdentity<T extendsLengthwise>(arg: T): T { console.log(arg.length); return arg; } // To limit generics must have a length property, extend it // using an interface with length property.
Triple-slash directives
Triple-slash directives are single-line comments containing a single XML tag at the top of a file, instructing compiler to do certain preprocessing.
Example: <refrence path="..." /> include additional files in the compilation process.
const k = Math.max(5, 6); // k = 6 const j = Math.mix(7, 8); // Error: Property 'mix' does not exist on type 'Math'.
// Hover mouse over max and you'll get its shape and description: // (method) Math.max(...values: number[]): number // Returns the larger of a set of supplied numeric expressions.
@param values — Numeric expressions to be evaluated.
How did Typescript know that max was present but mix wasn’t?
How did Typescript know the shape of max?
The answer is there are delcaration files describing these built-in functions.
Ctrl click Math.max in VSCode and its jumps into a file:
1 2 3 4 5 6 7
// path: [User directory]\AppData\Local\Programs\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib // file: lib.es5.d.ts /** * Returns the larger of a set of supplied numeric expressions. * @param values Numeric expressions to be evaluated. */ max(...values: number[]): number;
This is the declaration file for es5 in Typescript.
Concepts
A declaration file provides a way to declare the existence of some types or values without actually providing implementations for those values.
Usually, a declaration file describe the shape of existing Javascript code, make them able to work in Typescript environment, enables typechecking and code completion and so on.
Typescript mainly has two kinds of files:
.tsimplementation files. Produce .js code and is where you normally write your code.
.d.tsdelcaration files. Only contain type information. Don’t produce js code, only used for typechecking.
Built-in declaration files is named with pattern lib.[name].d.ts
Declare global
To declare a global variable in Typescript, create a .d.ts file and use declare global{} to extend global object with typings for the necessary properties or methods. Typescript looks for .d.ts file in the same place it looks for your regular .ts file.
// index.ts global.country = 'Germany'; console.log(global.country); // Germany console.log(country); // Germany console.log(window.country) // For browser environment, use window instead of global
Ambient Modules
In Node.js, most tasks are accomplished by loading one or more modules. We could define each module in its own .d.ts file with top-level export declarations, but it’s more convenient to write them as one larger .d.ts file. To do so, we use a construct similar to ambient namespaces, but we use the module keyword and the quoted name of the module which will be available to a later import. For example:
{ "compilerOptions":{ // module system for the program, e.g. commonjs for node.js project "module":"commonjs", }, // specific files to include, does not support patterns as opposed to "include" property "files":[ "core.ts", "sys.ts", "types.ts", "scanner.ts" ], // By default all @types packages are included. Packages in node_modules/@types of any enclosing folder are considered visible. // If types is specified, only packages listed will be included in the global scope. "types":["node","jest","express"], // If typeRoots is specified, only packages under typeRoots will be included. For example: "typeRoots":["./typings","./node_modules/@types"] // To distinguish between types and typeRoots, read this: // https://stackoverflow.com/questions/39826848/typescript-2-0-types-field-in-tsconfig-json
// include/exclude: specifies an array of filenames and patterns to include/exclude in the program // ** matches any directory nested to any level "include":["src/**/*","tests/**/*"], "exclude":["dist","node_modules"] }
Declaration Merging
Declaration merging merges two or more separate declarations with same name into a single definition.
// iteration with for for (let direction of directions2) { console.log(direction) }
// iteration with forEach directions2.forEach(direction => { console.log(direction) })
Debug with VS Code
DefinitelyTyped
TypeScript allows you to have Declaration Files which are files that allow you to describe the shape of code that’s written in (for example) plain JavaScript. So, by referencing one of these files you tell TypeScript exactly how that JavaScript code or library you are using should be assumed to be “typed”.
Of course, this means that the declaration file need to be carefully written and in sync with the JavaScript library you are using.
DefinitelyTyped is the most popular repository of Declaration Files for many many JavaScript libraries most of which do not provide their own declaration files (as they are not developed with TypeScript and are not committed to work with it). So it holds Declaration files maintained by the community.