Programming TypeScript - KeynesYouDigIt/Knowledge GitHub Wiki
Type Safety: Using types to prevent programs from doing invalid things
Typechecker: A special program that verifies that your code is typesafe
Your code goes TS -> TS AST -> Typechecker checks AST -> JS
Type system: A set of rules that a typechecker uses to assign types to your program.
"You see this value here? Here's it's type!"
Each project should include:
.tsconfig.json
- Generate with
npx tsc --init
// .tsconfig.json
{
"compilerOptions": {
"lib": ["es2015"], // What exists in the environment?
"module": "commonjs", // What module system are you using?
"outDir": "dist", // Where should your .js code go?
"sourceMap": true,
"strict": true, // Require all of your code to have types
"target": "es2015" // Which JS version should your code compile to?
},
"include": [ // Where are your .ts files?
"src"
]
}
eslint.json
- Generate with
npx eslint --init
index.ts
Type: A set of values and the things you can do with them. Knowing a type lets you know valid values and valid operations.
Example: A Boolean is the set of all booleans (true and false), and the operations you can perform on them (||, &&, !)
- Annotate: Add the type to a value
- Constrain: Restrict a parameter to a value
- Bounds: Upper and lower values for something
- Assignable: An argument being compatible with a type
- Type literal: A type that represents a single value and nothing else. Good for maximizing type safety.
- Structural typing: A style of programming where you just care that an object has certain properties, and not what its name is (nominal typing). Also called "Duck Typing" in some languages.
- Definite assignment: Making sure that a value has been assigned before its used.
-
Index signature:
[key: T]: U
- "All keys of type T must have values of type U."
any
: Set of all values, which you can do anything with. Use a last resort. Top type.
unknown
: Set of all values, but can only do comparison. If you check the type with if (typeof thing === 'number')
, you can do type-specific operations. Will get Better than any
.
boolean
: Two values, you can compare them or negate them. You can annotate it as being a specific boolean (type literal), otherwise can always be inferred.
number
: All numbers, NaN
, and Infinity
. All numeric operations. Also can always be inferred.
bigint
: New, very large numbers.
string
: Set of all strings, can concat, slice, etc. Also can always be inferred.
symbol
: Can be annoted as unique. Rare. Just like a type literal.
object
: Must specify the shape of the object. Can be inferred. Inferring doesn't do literal values since values are reassignable. Avoid emtpy objects, TS won't infer them correctly.
Arrays- T[]
or Array<T>
, eg string[]
: Explicitly type. Typescript won't narrow your types if you create an array with an empty literal, it will just add each type to its inference. Keep arrays homogenous, or you have to type check them every time you want to do something with one of them.
Tuples- [T...]
, eg. [string, boolean]
: Mixed types. Have to declare up front or TS will assume its an array.
null
: One value. Absence of value, eg. an error prevented something from being calculated. It's the subtype of all types except never
, which means that every type is nullable, which means you always have to check if something exists first. strictNullChecks
prevents this.
undefined
: One value. Never assigned.
void
: No explicit return value (eg. console.log
)
never
: No values. A function that never stops running, an exception throwing. The "bottom type," a proposition that's always false.
Structure: [key: keyType]: valueType
let a {
b: number
c?: string // Optional
readOnly d: number // `const` for properties
readOnlyArray<string> // Optional syntax
[key: number]: boolean // An unknown number of properties that have numbers for keys and booleans for values
[someString: string]: string // A
}
You can use the type
keyword to assign a type to a value. Makes your types more expressive. You can switch an alias for its type interchangeably. You can't declare a type twice. They're scoped and shadowed.
type Age = number
const kyle = {
age: Age
}
Union is combined types, intersection is shared types. Unions are more common.
type Cat = { name: string, meows: boolean }
type Dog = { name: string, barks: boolean }
type CatOrDogOrBoth = Cat | Dog
type CatAndDog = Cat & Dog
A functions returned value might be something like string | boolean
Fixed range of values. Don't use them in code that you publish, or it might conflict with someone else's. They are difficult to use safely, and can be avoided.
enum Language = {
English = 100,
Spanish = 200,
Russian // Infers 301
}
Language.English
Language[200]
Or (safer):
const enum Language = {
English,
Spanish,
Russian
}
Language.English
- TypeScript generally can't infer parameters, but can often infer return type
- Parameters can be optional (must go last) or have defaults (don't need to be explicitly typed)
-
arguments
andfunction
are unsafe
- Formal Parameter - Parameter
- Actual Parameter - Argument
- Type Level Code - Valid TypeScript, but not valid JavaScript on its own
- Value Level Code - Valid JavaScript
- Contextual Typing - Using a function signature type to get the types of parameters and return values
- Overloaded function - A function with multiple call signatures
- Concrete Type - A definite type, like boolean, Date[], { a: number } | { b: string }, (numbers: number[]) => number
- Generic Type Parameter - A placeholder type used to enforce a type-level constraint in multiple places. Also called a polymorphic type parameter.
- Upper Bound - A type that another type must have somewhere in its hierarchy
function add(a: number, b: number): number {
return a + b
}
function add(...numbers: number[]) {
return numbers.reduce((sum, number) => sum += number)
}
type someObject = {
someKey: "someValue"
someOptionalKey?: "someOtherValue"
}
function someFunction(): someObject {
// Whatever
}
function typeThis(this: someObject): void {
}
// Typing a function signature
// When you do this, you don't need also annotate the parameters in the function definition ("contextual typing")
type Greeting = (message: string) => void
// Overloaded function signature
type Reserve = {
(from: Date, to: Date, destination: string): Reservation
(from: Date, destination: string): Reservation
}
// When you implement this, you need to combine it into one type:
let reserve: Reserve = (from: Date, toOrDestination: Date | String, destination?: string) => {
// And then check to see which one it is:
if (toOrDestination instanceof Date && destination !== undefined){
// it's `to`
}
}
Generics are placeholders for type. You can indicate that multiple things need to be the same type, without specifying what they are ahead of time.
type Filter = {
<T>(array: T[], fn: (item: T) => boolean): T[]
}
type Map = {
<T, U>(array: T[], fn: (item: T) => U): U[]
}
type Whatever = <T = string>(T): boolean // default value
- Generics are like placeholder types
-
<T, U, V>
are the TS conventions, but they can be anything, including descriptive words - Concrete types are bound at compile time, and are compiled differently for each instance
- Generic types generally inferred well, but may need to be explicitly bound as well (eg. Promises resolution values)
Generics can be scoped two different ways:
// Per signature
type Filter = {
<T>(array: T[], fn: (item: T) => boolean): T[]
}
// or
type Filter = <T>(array: T[], fn: (item: T) => boolean): T[]
let filter: Filter = //
// For all signatures
type Filter<T> = {
(array: T[], fn: (item: T) => boolean): T[]
}
// or
type Filter<T> = (array: T[], fn: (item: T) => boolean): T[]
let filter: Filter<number> = //
You can also use generics in type aliases, and pass those like parameters:
type SomeType<T> = {
someKey: T
someOtherKey: string
}
type SomeOtherType<T> = {
someKey: SomeType<T>
someOtherKey: boolean
}
type SomeConcreteType = SomeOtherType<string>
You can "bound" generics to at least be some concrete type (including joins and intersections), which preserves the original type while still bounding it.
type NodeMapper = <T extends TreeNode>(item: T[]): T
type NodeMapper = <T extends TreeNode & OtherNode>(item: T[]): T
- Adds access modifiers, readonly
- Abstract classes can include overrideable methods
- Interfaces are lighter and just define a shape and can be extended, and can't include type expressions
- Interfaces can be declared multiple times and will merge
- Interfaces can take in generics, but all generics must have the same type and name
-
this
can be a return type - Classes are structurally typed- that means it doesn't check to see if the names are the same, only if the types are assignable
- Types and values have different namespaces, except for classes and enums, which uses the same for both
- Classes generate types for both the Class itself as well as the constructor
- Classes can have generics, as can methods
class Piece<K, V> implements SomeInterface, SomeOtherInterface {
constructor(
private readonly color: Color,
file: File,
rank: Rank,
){
this.position = new Position(file, rank)
}
something = (K): V => {
}
}
- A mixin is just a function that takes in a class constructor and returns a class constructor
// Declares a new type that takes in a generic
// It is equal to a constructor that takes in any arguments, and returns that generic
type ClassConstructor<T> = new(...args: any[]) => T
//the function addDebug, which declares a generic, which gives a value for T
function addDebug
<C extends ClassConstructor
<{ getDebugValue(): object }>
>
// Takes in a class as an argument, it needs to be a class constructor
(Class: C) {
// Returns an anonymous class that extends the target class and has a debug method
return class extends Class {
debug(){
let Name = Class.constructor.name
let value = this.getDebugValue()
return `${Name}(${JSON.stringify(value)})`
}
}
}
const User = new addDebug(SomeClass)
const someUser = new User("a", 1, false)
someUser.debug()
Wrap a class and return a new one with new functionality. Not quite ready for primetime yet.
function serializable<
T extends ClassConstructor<{
getValue(): Payload
}>
>(Constructor: T) {
return class extends Constructor {
serialize() {
return this.getValue().toString()
}
}
}
@serializable
class APIPayload {
getValue(): Payload {
//
}
}
- Union types increase safety
let Shoe {
create(type: 'boot' | 'sneaker' | 'dress'): Shoe {
switch(type){
case 'boot': return new Boot
case 'sneaker: return new Sneaker
case 'dress: return new DressShoe
}
}
}
class RequestBuilder {
private url: string | null = null
private data: object | null = null
setURL(url: string): this {
this.url = url
return this
}
setData(data: object): this {
this.data = data
return this
}
}
new RequestBuilder()
.setUrl("google.com")
.setData({})
- If something is a subtype, you can use it anywhere the supertype is required
-
never
is a subtype of everything, and a supertype of nothing -
any
is a subtype of everything
-
- Figuring out this relationship is trickier once generics get involved.
For purposes of the book:
-
A <: B
means "A is a B or a subtype" -
A >: B
means "A is a B or a supertype"
4 types of variance:
- Invariance: You want exactly a
T
- Covariance: You want
<:
aT
- Contravariance: You want
>:
aT
- Bivariance: You want either
>:
or<:
aT
Typescript always expects you to use types that are the same or subtypes of the ones you declare ("covariance"), except function parameters, which expect the same or supertypes of what you declare ("contravariance").
A function is a subtype of another function if:
- Same or lower arity
- Its
this
type isn't specified or is>:
- Each of its parameters are
>:
- Its return type is
<:
The reason the parameters need to be broader for it to be subtype is that if you're going to swap the subtype function anywhere a supertype function is expected, you can't have things in the function that the supertype might be able to pass in.
For non-enums, something is assignable if it's <:
, or the type is any
. For enums, something is assignable if it's in the enum, or if it's a number and one of the members of the enum is a number. This is kind of dangerous, so enums are best avoided.
If you use const
, the type is the literal value. If you use let
, it's the base type for the value. You can explitly annotate to narrow it. Reassigning a type from const
to let
widens it. null
and undefined
are widened to any
, but given definite types when they leave the scope (like by invoking a function). You can opt out of type widening (recursively!) with as const
.
let a = 1 // number
const b = 1 // 1
let c: 1 | 2 = 1 // 1 | 2
let d = { a: { b: "c" }} as const // { readonly a: { readonly b: "c" }}
TS will catch if you add a property to an object that wasn't on the type.
TS does "flow-based type inference", which means it follows the logic of your app and narrows things like union types to their specific types.
Flow-based type inference will pick out specific types, but has limits to its logic:
type a = {a: number, b: string}
type b = {a: array, b: boolean}
type aOrB = a | b
function (someparameter: aOrB){
if (typeof someParameter.a === "number"){
someParameter.a // number
someParameter.b // string | boolean, uh oh
}
}
To fix this, add an extra artificial property that discriminates between them:
type a = {type: "a", a: number, b: string}
type b = {type: "b", a: array, b: boolean}
type aOrB = a | b
function (someparameter: aOrB){
if (someParameter.type == "a"){
someParameter.a // number
someParameter.b // string
}
}
Also called "exhaustiveness checking." TypeScript checks for unhandled cases and code paths that don't return values.
Pull out types from (for example), an API response:
type Owner = {
name: "Kyle",
dogs: [{
name: "Bixby",
breed: "Chow"
}]
}
type Dogs = Owner["dogs"]
type Dog = Owner["dogs"][number] // Keys into an array
Dot notation doesn't work for this.
Make a union of all of an object's keys:
type someObject = {
a: 1,
b: 2,
}
type someObjectKeys = keyof someObject // "a" | "b"
These allow you to map through the keys of an object and transform their types:
type Readonly<T> = {
readonly [K in keyof T]: T[P]
}
type ReadOnlyDog = Readonly<Dog>
These allow you to type keys and all values, for things like dictionary lookups.
type ProductID = string
type AvailabilityTypes = 'sold_out' | 'in_stock' | 'pre_order'
interface Availability {
availability: AvailabilityTypes
amount: number
}
const store: Record<ProductID, Availability> = {
'0d3d8fhd': { availability: 'in_stock', amount: 23 },
'0ea43bed': { availability: 'sold_out', amount: 0 },
'6ea7fa3c': { availability: 'sold_out', amount: 0 },
}
-
Partial<Object>
- Makes every field optional -
Required<Object>
- Makes every field required -
Readonly<Object>
- Makes every field read only -
Pick<Object, Keys>
- Returns a subtype of the object with only the given keys
Since types and variables have different namespaces, you can name an object and a type the same thing. It also lets you import then both at the same time:
// Currency.ts
type Currency = {}
class Currency extends Currency {
static exchange(){}
}
export Currency Currency
// App.ts
import { Currency } from "./Currency"
const someMoney: Currency = {}
const exchangedCurrency = Currency.exchange("USD")
Ordinary tuples infer pretty wide:
let a = [1, true] // (number | boolean)[]
Make function that spreads the arguments, and they'll get typed by position:
function tuple<T extends unknown[]>(...types: T): T {
return types
}
let a = tuple([1, true]) // [number, boolean]
Type refinement only works within the scope you're in. So this won't work:
function isString(a: unknown): boolean {
return typeof a === "string"
}
function someFunction(a: string | number){
if (isString(a)){
a.toUpperCase() // Error, can't call that on numbers
}
}
To fix that, you can use the is
keyword. This means that the return value is boolean, and the parameter you passed in should be refined. You can only use this with one parameter.
function isString(a: unknown): a is string {
return typeof a === "string"
}
Types with ternaries. If something is a subtype, assign to A, otherwise to B.
type IsString<T> = T extends string
? true
: false
Distributive property applies.
(string | number) extends T ? A : B
// is the same as
(string extends T ? A : B) | (number extends T ? A : B)
A way to do a generic parameter.
type SomeType<T> = T extends (infer U)[] ? U : T
-
Exclude<T, U>
- Get the typesT
that are not inU
-
Extract<T, U>
- Get the typesT
that are assignable toU
-
NonNullable<T>
- Excludenull
andundefined
fromT
-
ReturnType<F>
- Calculate a function's return type -
InstanceType<C>
- Get the instance type of a class constructor
Use rarely.
Tell Typescript to assign a related type:
let someVariable: string | number = "hi"
someFunction(someVariable as string)
someFunction(someVariable as any) // Super unsafe
You can tell the typechecker that you're confident that something isn't null or undefined with !
:
type Something = {
id?: string
}
let something: Something = { id: 2 }
find(something.id!)
This is usually a sign that you need some refactoring to multiple types.
Tell the typechecker that something will definitely have a value before it's used:
let userId!: string
assignId()
userId.toUpperCase()
A technique called "type branding":
type SpecificTypeOfID = string & { readonly brand: unique symbol }
function SpecificTypeOfID(id: string){
return id as SpecificTypeOfID
}
Rare to use, but keeps different kinds of IDs from getting accidentally interchanged in a large system and causing logic errors.
Ways to handle errors:
- Return
null
- Doesn't tell you why something failed
- It's awkward to check for null every time
- Throw exceptions
- Tells you why something failed
- Types can't ensure that all exceptions are accounted for
- Return exceptions and make return type a union of the happy path and each exception
- Tells you why something failed
- Forces developers to handle exceptions
- Verbose
-
Option
type- Allow you to chain operations if one of them might fail
- Doesn't tell you why something failed
- Not interoperable with code that doesn't use monads
function somethingThatCouldError(): boolean | SomeException | SomeOtherException {
//
}
const result = somethingThatCouldError()
if (result instanceof SomeException) // do something
if (result instanceof SomeOtherException) do something
Instead of returning a value, you return a container that might have a value or might not. The container has some methods that let you chain an operation or return a value.
Examples:
Try
Either
-
Option
/Maybe
- What we're doing
interface Option<T> {
flatMap<U>(someFunction: (value: T) => None): None
flatMap<U>(someFunction: (value: T) => Option<U>): Option<U>
getOrElse(value: T): T
}
// A successful operation that resulted in a value
class Some<T> implements Option<T> {
constructor(private value: T){}
// Overloaded signature
flatMap<U>(someFunction: (value: T) => None<U>): None
flatMap<U>(someFunction: (value: T) => Some<U>): Some<U>
flatMap<U>(someFunction: (value: T) => Option<U>): Option<U> {
return someFunction(this.value)
}
getOrElse(): T {
return this.value
}
}
// An operation that failed and doesn't have a value
class None implements Option<never> {
flatMap<U>(): None{
return this
}
getOrElse<U>(value: U): U {
return value
}
}
// Overloaded signature
function Option<T>(value: null | undefined): None
function Option<T>(value: T): Some<T>
function Option<T>(value: T): Option<T> {
return (value == null)
? new None
: new Some(value)
}
let result = Option(someValue)
.flatMap(someFunction)
.flatMap(someOtherFunction)
.getOrElse("Whoops")
Promise types: Promise<ResolvedValue, ErrorType>
You can handle emitters by overloading the signature:
type RedisClient = {
on(event: "ready", f: () => void): void
on(event: "error", f: (e: Error) => void): void
on(
event: "reconnecting",
f: (params: { attempt: number, delay: number }) => void
): void
}
By having the event type narrowly typed, we can further narrow the function signature. You can also do this with mapped types:
type Events = {
ready: void
error: Error
reconnecting: { attempt: number, delay: number }
}
type RedisClient = {
on<E extends keyof Events>(
event: E,
f: (arg: Events[E]) => void
): void
emit<E extends keyof Events>(
event: E,
arg: Event[E]
): void
}
This is common, terse, non-repetitive, and good.
- Add the support you need to your
tsconfig.json
(eg."compilerOptions": { "lib": ["dom", "es2015"] }
)- This doesn't add any extra code, just tells the compiler what things it should let go
- TSX = JSX with TS support
- Enable with
compilerOptions: { "jsx": "react" }
(compiles to a.js
file) - Can also do "react-natve" and "preserve" (typecheck, but leave the file alone)
- Enable with
- TS ensures that JSX is well-formed and the required parts are being passed in
- You can't type SQL, but you can type your ORM calls
React:
import React from "react"
// Functional
type Props = {
isDisabled?: boolean
someOtherProp: string
}
export default function SomeComponent(props: props){
return ()
}
// Class
type Props = {
isDisabled?: boolean
someOtherProp: string
}
type State = {
isActive: boolean
}
export default class SomeComponent extends React.Component<Props, State>{
}
History of modularization:
- 1995 - Globals, IIFEs
- 2004 - Dojo (had a module loader)
- 2005 - YUI (had a module loader)
- 2008 - AMD
- 2009 - LABjs (had a module loader)
- 2009 - CommonJS
- 2011 - Browserify
- 2015 - ES2015 import/export
ES2015 modules can be statically analyzed- CommonJS modules cannot.
You can dynamically import a module with import()
, which returns a promise. This is only allowed in TS with the "module": "esnext" }
mode. You can pass any expression in, but you'll lose type-safety if it's not a string. To get around this, you can manually annotate the import:
import { locale } from "./locales/locale-us" // Type
async function something(){
const locale: typeof locale = await import(somePath)
}
You can allow default exports from CommonJS modules with { "compilerOptions": { "esModuleInterop": true }}
TS decides whether to parse a .ts
file in module mode or script mode based on whether or not there are any import
or export
statements.
TypeScript has a namespaces feature built-in, but should be avoided in favor of modules.
- A file that ends with
.d.ts
- Can only have types, no values
- Only for code that's visible to consumers (otherwise you could just use the TS code directly). You can compile your code and include type declarations, and now other TS projects don't need to also compile your code. They also provide hinting for editors.
- Can be generated with
tsc -d some-file.ts
- Can declare that types exist somewhere (eg. globals, imported modules)
- Can be use to generate "ambient types" that don't need to be imported every time
- Useful for models
- Use
types.ts
for ambient types in a TS project,filename.d.ts
for JS files
declare let process: {
env: {
NODE_ENV: 'development' | 'production'
}
}
declare module 'some-module' {
export type MyType = number
let myDefaultExport = MyType
export default myDefaultExport
}
declare module 'unsafe-module'
declare module 'something-with-wildcards*'
//
import someModule from "some-module" // Typed!
import unsafeModule from "unsafe-module" // All types are `any`
import something from "something-wild-wildcards-like-this"
- Add TSC to your project
- With
{ "compilerOptions": { "allowJs": true}}
- It won't typecheck, but it will transpile it
- Start type-checking existing JS code
- With
{ "compilerOptions": { "checkJs": true}}
- You may also want to allow implicit
any
s with{ "compilerOptions": { "noImplicitAny": false}}
- If this generates too many errors, you can throw
// @ts-check
at the top of a file to check just that file - You can ignore a particularly noisy file with
// @ts-nocheck
- You can typecheck individual functions with JSDoc
- Migrate JS to TS, one file at a time
- Either do each file one by one, or mass-rename and use toggling
strict
mode to find errors
- Install type declarations for your dependencies
- If the types are included by the authors, use them
- Otherwise, install the community types from DefinitelyTyped (
npm i @types/module-name
) - You can whitelist an untyped module once with
// @ts-ignore
which will type it asany
- You can whitelist an untyped module always by making an empty ambient module declaration
- You can create an accurate ambient module declaration
- You can contribute your ambient module declaration to DefinitelyTyped
- Turn on
strict
mode
- Disable JS
/**
* @param word { string } An input string to convert
* @returns { string } The converted string
*/
export function someFunction(word){
return word.toLowerCase()
}
- Look for a sibling
.d.ts
file with the same name as the JS file - Otherwise try to infer the types and use JSDoc annotations
- Otherwise, treat the whole module as
any
-
.js
files -
.js.map
- Source maps- Links each line of your generated code to the appropriate line of its TS source
-
.d.ts
- Type declarations- Lets others use your types
-
.d.ts.map
- Declaration maps- Speeds up compilation when you're using references
- Compile targets: Transpilation is done for many things, but not regex flags, object getters/setters, and BigInts.
- Lib: A list of types that TS can bring in (eg.
dom
,es2015
,es2015.array.includes
) - Include source maps (not emitted by default) for easier debugging
As the app gets bigger, you can TS compile parts of it separately with project references:
{
"compilerOptions": {
"composite": true, // Part of a larger project
"declaration": true, // Generate declarations
"declarationMap": true, // Build source maps for the declarations
"rootDir": "." // Compiled relative to this folder
},
"include": [
"./**/*.ts"
],
"references": [{
"path": "../myReferencedProject", // A dependency of this project
"prepend": true
}]
}
Make a root tsconfig.json
that brings together all your projects that aren't referenced by anything else (or they'll go in twice) and references them:
{
"files": [],
"references": [{
"path": "./SomeProject"
},{
"path": "./SomeOtherProject"
}]
}
Build everything with tsc --build
or tsc -b
. You can also make json files that extend other files to share settings.
.gitignore
your generated artifacts, and .npmignore
your source code. Put a types
key in your package.json
that indicates where the generated types are.
Type operator | Syntax | Use on |
---|---|---|
Type query |
typeof , instanceof
|
Any type |
Keys | keyof |
Objects |
Property lookup | O[K] |
Objects |
Mapped type | [K in O] |
Objects |
Add/Sub Modifier |
+ /-
|
Objects |
Read-only Modifier | readonly |
Objects, Arrays, Tuples |
Optional Modifier | ? |
Objects, Arrays, Tuples, Function Params |
Nonnull Assertion | ! |
Nullable types |
Generic Default | = |
Generic types |
Type assertion |
as , <>
|
Any type |
Type guard | is |
Function return types |
Type utilities | Used on | Description |
---|---|---|
ConstructorParameters |
Class constructors | Tuple of parameter types |
Exclude |
Union types | Exclude a type from another type |
Extract |
Union types | Select a subtype that's assignable to another type |
InstanceType |
Class constructors | The type you get from newing a constructor |
NonNullable |
Nullable types | Exclude null and undefined from a type |
Parameters |
Function types | Tuple of a function's parameter types |
Partial |
Object types | Make all properties in an object optional |
Pick |
Ojbect types | A subtype of an object type, with a subset of its keys |
Readonly |
Arrays, objects, tuples | Make all properties or keys readonly |
ReadonlyArray |
Any type | Make an immutable array of the type |
Record |
Object types | Map from a key type to a value type |
Required |
Object types | Make all properties in an object required |
ReturnType |
Function types | A function's return type |