What are the types of the variable x and y? #
let x = "hi";
const y = "hi"
Answer
x is of type string and y is "hi". This happens because const
can't change
so the only value possible is "hi". It's a literal
type.
let x = "hi";
const y = "hi"
x is of type string and y is "hi". This happens because const
can't change
so the only value possible is "hi". It's a literal
type.
let n = 5;
n = NaN;
Yes, NaN
will compile. NaN
is a number in JavaScript.
let a = 30;
function func(num: 30) {} // 👀
func(a);
No, this won't compile. The function only accepts num
where the value is 30.
type AB = {
a?: number;
b: number;
};
const x: AB = { b: 1 };
x.a = undefined;
x.a = 1;
interface CD {
c?: number;
d: number;
}
const y: CD = { d: 1 };
y.c = undefined;
y.c = 1;
Yes, the code above compiles.
An optional property (ending with ?
) will accept the undefined
value too.
By default, before initializing a property it has the undefined value, so there's no need to initialize for instance a
when assigning an object to the variable x
.
Interfaces and types work the same way regarding optional properties.
type Boat = { size: number }
type Marina = { [k: string]: Boat }
let marina: Marina = {}
// Note property titanic was not defined
marina.titanic.size = 300;
marina.mcBoat.size = 30;
Yes, it will compile.
But if you run the code, it will throw an error because titanic
is not defined.
let tuple: [number, number] = [1, 2];
tuple.push(3); // A
tuple = [1, 2, 3]; // B
It shows errors on line B.
let arr1:string[] // A
let arr2:[string] // B
let arr3:Array<string> // C
Line B
won't compile.
This is not a valid syntax to define arrays. Notice line C
will show string[]
type and not Array<string>
.
const mixedArray = [1, "Bob"]
The type is (number | string)[]
.
TypeScript can infer the possible types from the contents of the array.
type Boat = {
color: string;
};
type Boat {
engine: boolean;
}
No, types don't merge. This causes a duplicate identifier
error.
type Boat = {
color: string;
};
let b: Boat = {};
No, the Boat
object needs to always conform to the Boat
type. If you need to partially build an object, you can use the utility Partial
and then use a type guard to make sure the object is completely built.
let s1: string;
let s2: String;
s1 = s2; // A
s1 = String(1) // B
Line A
will not compile because string
and String
are different types.
type Result = Error | 42;
let r: Result = new Error("boom");
r = 42;
Yes, it compiles.
type Result = string | number;
let r = "banana"; // 👀
r = 42;
No, it doesn't compile. The danger of implicit typing is that sometimes you might forget to add them. This works:
type Result = string | number;
let r: Result = "banana"; // Notice r is explicitly typed with `Result`
r = 42;
a
? #type Res = ["error", Error] | ["ok", string];
function f(): Res {
return Math.random() >= 0.5
? ["error", Error("boom")]
: ["ok", "some message"];
}
const [a] = f();
The type of the variable a
is "error" | "ok"
. TypeScript can calculate the type for you.
type T1 = {
a: string;
};
type T2 = {
b: number;
};
let c: T1 & T2;
c = { a: "a", b: 1 };
Yes, it compiles. Intersections create a type that is the combination of both types.
let c: string & null;
c = "hi";
No, it doesn't compile.
c
is of type never
because something cannot work as type string
and type null
. The types are in conflict.
interface Boat {
color: string;
}
interface Boat {
engine: boolean;
}
const b = { color: "white", engined: true };
Yes. The interfaces merge and there are no errors. The variable b
complies with the interface Boat
.
type Boat = {
color: string;
};
interface Boat {
engine: boolean;
}
No, because there's a duplicate identifier Boat
. And you can't merge interfaces with types.
type AType = {
prop: string;
}
type BType = {
prop: number;
}
type CType = AType & BType; // A
type MyProp = CType['prop']
interface AInterface {
prop: string;
}
interface BInterface {
prop: number;
}
interface CInterface extends AInterface, BInterface { // B
}
Line A
compiles, but line B
doesn't compile.
Currently, extending interfaces catches more conflicts than type intersections. Read more about it here.
const x: unknown = "whatever";
const y: any = x;
const z: unknown = x;
Yes, it will compile.
It's possible to assign unknown
to any
and vice-versa.
let x:any;
x.f()
Yes.
any
doesn't check the type.
let x:unknown;
x.f()
No.
Although you can put anything an unknown
type, you need to cast it first.
type HasName = { name: string }
type User = { name: string, age: number }
const x: User = { name: 'Jamie', age: 40 }
const y: HasName = x;
It will compile.
It's possible to assign a type to another type more restrictive.
function f(n: number | string) {
n.toFixed(2);
}
No, it won't compile. The n
must be narrowed to a single type. Like this:
function f(n: number | string) {
if (typeof n === "number") n.toFixed(2)
}
function f(n: string | string[] | null) {
if (n && typeof n === "object") {
console.log(n); // A
}
}
The type of n
is string[]
.
You have to check if n
is truthy because typeof null
is object
.
This means that if the variable is null
or string[]
, it will have the type "object".
function f(x: string | number, y: string | object) {
if (x === y) {
x; // A
}
}
The type of x
is string
.
If 2 variables are equal (by using ===
), then both variables must be of the same type.
In this case, the only possibility is to be string
.
function f(x: boolean | null | undefined) {
if (x == null) {
x; // A
}
}
The type of x
is null | undefined
.
It's possible to narrow types by using loose equality.
If you loosely compare a value to null, it can be null
or undefined
like in the case above.
type Boat = { engine: string };
type Car = { color: string };
function f(x: Boat | Car) {
if ("engine" in x) {
x; // A
}
}
The type of x
is Boat
.
It's possible to narrow types by using the in
operator.
class Boat {
constructor(public name: string) {}
}
function f(x: Boat | string) {
if (x instanceof Boat) {
x; // A
}
}
The type of x
is Boat
.
It's possible to narrow types by using the instanceof
operator.
type Boat = { name: string }
function isBoat(value: any): value ??? Boat {
return value && "name" in value;
}
let maybeBoat: unknown
if(isBoat(maybeBoat)) {
maybeBoat // A maybeBoat is Boat
}
???
is the keyword is
, a type guard.
maybeBoat
in line A? #type Boat = { make: string; model: string };
function assertIsBoat(valueToTest: any):
asserts valueToTest is Boat {
const cond =
valueToTest &&
typeof valueToTest === "object" &&
"make" in valueToTest &&
typeof valueToTest["make"] === "string" &&
"model" in valueToTest &&
typeof valueToTest["model"] === "string";
if (!cond) throw new Error("Value is not a Boat");
}
let maybeBoat: unknown;
assertIsBoat(maybeBoat);
maybeBoat; // A
The type of maybeBoat
is Boat
.
Although if you run the code, it will thrown an error because maybeBoat
is udnefined.
car
in line A? #type Boat = { make: string; model: string };
type Car = { seats: number };
function assertIsBoat(valueToTest: any):
asserts valueToTest is Boat {
const cond = valueToTest &&
typeof valueToTest === "object";
if (!valueToTest) throw new Error("Value is not a Boat");
}
let car: Car = { seats: 4 };
assertIsBoat(car);
car; // A
The type of car
is Car & Boat
.
Even when the assertion is not good (like in the code above) TypeScript will trust you.
function g(n: number | null) {
const num: number = n;
}
No, the code won't compile. Because you can't assign a value that is less strict. And in this case the value n
accepts number
or null
while num
only accepts numbers.
But you can assign a more strict value to a less strict variable:
function f(n: number) {
const num: number | null = n;
}
export default ({ children }) => <PageLayout>{children}</PageLayout>;
type T = { a: 1 }
let t1 = {} as T; // A
let t2: T = {} // B
No, the code won't compile for line B
.
There are some situations when using as
that will make the code more permissive like in the example above.
Learn more about type assertions.
interface TwoNumsFn {
// A
}
const add: TwoNumsFn = (a, b) => a + b
You should put (a: number, b: number): number
.
type TwoNumsFn = // A
const add: TwoNumsFn = function (x, y) {
return x + y;
}
const sub: TwoNumsFn = (a, b) => a - b
(x: number, y: number) => number
type Boat = {
location?: { country: string }; // Notice it's optional
};
const b: Boat = {};
b.location!.country = "Luxembourg";
Yes, the code will compile.
But it will throw an error during runtime because b.location
is undefined
.
class C {
x!: number;
}
The code will compile.
The x!
will suppress any errors even if the class field was not initialized.
function f(n: number): undefined {
return undefined;
}
Yes. If you explicitly return undefined
.
function f():___ {
}
The keyword void
.
function f(): undefined {
console.log(1)
}
console.log(f()) // Logs "undefined"
No, the code won't compile.
Even though functions that don't have return statements return undefined
,
when we say the function is going to return undefined
there needs to be somewhere an explicit undefined
return.
function f() {}
const x = f()
It's void
.
class Boat {
color: string
engine: boolean
constructor(color: string) {
this.color = color
Promise.resolve().then(()=> this.engine = true)
}
}
Yes, there is an error because engine is not initialized.
The constructor is initializing the engine
field but not immediately. This means users of class Boat
are not guaranteed to have the engine
field initialized.
// abstract classes are like "half a class".
// In the case below, classes that extend Boat must implement the `engine` field.
abstract class Boat {
public abstract engine: string
color: "white"
}
class Titanic extends Boat {
public engine: string | string[] = ["big engine", "small engine"]
}
No, it will not compile. The engine
field has to be compatible with the abstract class.
abstract class Boat {
public abstract color: string
}
class Titanic extends Boat {
public color: "white" = "white" // Notice the literal type
}
Yes, it will work without errors.
You can constrain a type but you can't relax it. The color
in Titanic
can only be the string "white".
type Boat = {
color: string;
};
class Titanic implements Boat {
constructor(public color: string) {}
}
Yes, it will compile. A new Boat object will have the field color
.
The public syntax is shorthand for the typical object initialization. This is class property inference from constructors. The code above is the same as this:
type Boat = {
color: string;
};
class Titanic implements Boat {
color: string;
constructor(color: string) {
this.color = color;
}
}
class Boat {
color: string = "white";
}
export interface Boat {
engine: string;
}
const p = new Boat();
p.color;
p.engine;
No, and it will not compile. If a part of a merged declaration is exported, all the parts must be exported.
To make it work, remove the export
from the code above. It will also work if you add export to the class:
export class Boat {
color: string = "white";
}
export interface Boat {
engine: string;
}
const p = new Boat();
p.color;
p.engine;
class Boat {
private engine: number;
constructor(engine: number) {
this.engine = engine;
}
}
class Titanic extends Boat {
private engine: number;
constructor(engine: number) {
super(engine);
this.engine = engine;
}
}
No, it won't compile. "Types have separate declarations of a private property 'engine'"
This happens because private fields are public at runtime. So both fields can be accessed and that creates a conflict.
class Boat {
#engine: number;
constructor(engine: number) {
this.#engine = engine;
}
}
class Titanic extends Boat {
#engine: number;
constructor(engine: number) {
super(engine);
this.#engine = engine;
}
}
Yes, this code will compile. Contrary to the private
keyword, private hashed times (e.g. #engine
) are private at runtime.
class Boat {
color: string
engine: boolean
constructor(color: string) {
this.color = color
}
}
No, this doesn't compile. Because the engine
property has no initializer and it's not defined on the constructor.
type Boat = {
engines: string[];
};
function findEngine(engine: string) {
return this.engines.indexOf(engine) !== -1;
}
No, it won't compile.
There is an assumption that this
has an engines
field. But this
could be anything, so we cannot assume that.
We can fix it by defining the type of this
:
type Boat = {
engines: string[];
};
function findEngine(this: Boat, engine: string) {
return this.engines.indexOf(engine) !== -1;
}
class Boat {
f1: number;
public f2: number;
protected f3: number;
private f4: number;
constructor(f1: number, f2: number, f3:number, f4: number) {
this.f1 = f1;
this.f2 = f2;
this.f3 = f3;
this.f4 = f4;
}
}
const b = new Boat(1,1,1,1)
b.f1 // A
b.f2 // B
b.f3 // C
b.f4 // D
C and D won't compile.
A compiles because public
is the default access modifier.
class Boat {
constructor(f1: number, public f2: number, protected f3: number, private f4: number) { }
}
const b = new Boat(1, 1, 1, 1)
b.f1 // A
b.f2 // B
b.f3 // C
b.f4 // D
A, C and D won't compile. A won't compile because it's being parsed as a typical constructor parameter.
class Boat {
constructor(public f1: number, protected f2: number, private f3: number) { }
}
class Titanic extends Boat {
f() {
this.f1; // A
this.f2; // B
this.f3; // C
}
}
const t = new Titanic(1, 1, 1)
t.f1 // D
t.f2 // E
t.f3 // F
The lines C, E and F.
The protected
field is only accessible from within the subclasses of Boat
.
class Boat {
readonly f1: number;
constructor(f1: number) {
this.f1 = f1; // A
}
}
const b = new Boat(1)
b.f1 = 2 // B
There will be an error on B.
Readonly fields can be initialized on the constructor. Remember, there won't be any runtime check.
interface Boat {
color: string;
}
function logBoat(mcBoat: Boat) {
console.log(mcBoat);
}
logBoat({ color: "white", engine: false });
The code above won't compile.
Excess properties are checked for object literals.
The reason is logBoat
is receiving an object literal with a field (engine
) that will never be used.
It would work if the variable could be used for something else. This works:
interface Boat {
color: string;
}
function logBoat(mcBoat: Boat) {
console.log(mcBoat);
}
const reusableVar = { color: "white", engine: false };
// This is valid because reusableVar can be used in some other context
logBoat(reusableVar);
function f___(arr: T[]): T {
return arr[0];
}
const first = f([1, 2])
You should put <T>
.
The function look like this:
function f<T>(arr: T[]): T {
return arr[0];
}
const arr: ___<number> = []
arr.push(1)
The word to use is Array
.
Consider that Array<some_type>
is the same as some_type[]
.
interface Small<T, U> {
f1: T;
f2: U;
}
function f<T, U>(x: T, y: U): Small<T, U> {
return { f1: x, f2: y }
}
const x = f(1, "a")
The type of x
is Small<number, string>
.
const identity = <T>(val: T) => val // A
const identity = (val: T)<T> => val // B
identity(3) // returns 3
Line A
.
Like in regular functions, the generic type definition comes before the parentheses for the parameters.
const id = <T>(val: T) => val
id(1) // A
id<string>(1) // B
The code won't compile on line B
.
Because you narrowed the type of the function (by passing <string>
) you will get a compilation error warning you the argument of type 'number' is not assignable to parameter of type 'string'.
const sleep = <T>(val: T, ms: number) => {
return new Promise<T>((res) => setTimeout(() => res(val), ms))
}
sleep<number>("hey", 2000).then((val) => console.log(val))
The code won't compile.
To make it compile, you'd have to change the sleep call to accept strings. Like so:
sleep<string>("hey", 2000)
function f(n:number): number | Error {
if(n === 3) throw new Error("I don't like 3");
return n;
}
function g(n:number): number {
if(n === 3) throw new Error("I don't like 3");
return n;
}
Yes.
Any function can thrown an error.
function getErrorMessage(error: unknown): string {
return (error instanceof Error) ? error.message : String(error);
}
function f() {
try {
throw new Error('Oh no!')
} catch (error) {
console.log(error.message); // A
}
}
function g() {
try {
throw new Error('Oh no!')
} catch (error) {
console.log(getErrorMessage(error)) // B
}
}
No, line A will not compile.
The type of error is unknown
. Learn more from Kent.
Learn more:
enum DirectionEnum {
Up = "up",
Down = "down",
}
const enumFn =(dir: DirectionEnum) => dir
enumFn(DirectionEnum.Up) // output
enumFn("up") // output
The result is a type error.
The second push cannot infer the type.
type DirectionLiteral = "up" | "down"
const literalFn = (dir: DirectionLiteral) => dir
literalFn("up") // output
The output is "up".
In this case, "up" can be inferred as being of type "DirectionLiteral". There is no need to generate code as it happens with enums.
enum DirectionEnum {
Up = "up",
Down = "down",
}
type DirectionLiteral = "up" | "down"
const enumFn = (dir: DirectionEnum) => dir
const literalFn = (dir: DirectionLiteral) => dir
const b:string = "up" // notice it's a string
if (b === "up") {
enumFn(b); // output
literalFn(b); // output
}
The output is a type error on the first output.
You can't narrow types for an enum by comparing directly to a string.
const enum Directions { Up, Down }
const dirs = [
Directions.Down,
Directions.Down,
Directions.Up
]
console.log(dirs)
[1, 1, 0]
.
When using const enum
all the enum code is erased during compilation and replaced by its corresponding value.
enum DirNum { Up, Down }
enum DirStr { Up = "up", Down = "down" }
console.log(DirNum.Up)
console.log(DirStr.Up)
The log is 0
and up
.
// typings.d.ts
XXX module 'lodash' {
export function random(a: number, b: number): number;
}
// index.ts
import { random } from 'lodash';
random('banana'); // A
declare
.
To describe the shape of libraries not written in TypeScript, we need to declare the API that the library exposes. More info on so and official deep dive
Instead of defining the types yourself, you can use Definitely Typed
// strictNullChecks = false
let x: boolean = null;
let y: boolean = undefined;
The code will compile.
null
and undefined
are considered a subtype of any type when --strictNullChecks
is set to false.
This means type T
and T | undefined
or T | null
are considered synonymous.
If you want to learn more, visit the TypeScript documentation or Mike North's courses.