-
Notifications
You must be signed in to change notification settings - Fork 43
Typescript: (2) Creating Basic Types
const numbers: Array<number> = []
const item: string[] = []
function logScores(firstName: string, ...scores: number[]){
console.log(firstName, scores)
}
logScores("Mike", 90, 60, 65)
// logScores("Mike", 90, 60, "65")
// Argument of type 'string' is not assignable to parameter of type 'number'.
// you have to annotate this
const tomScore: [string, number] = ["tom", 70]
// because if you don't, it will infer it to be
// of type (string | number)[]
Open ended tuple - items have some structure but the number of items isn't fixed
const benScores: [string, ...number[]] = ["ben", 50, 75, 85]
const tomScore = { name: "tom", score: 70 }
// type will be infered as {name: string; score: number}
tomScore.score = 75 // this will not trigger an error
tomScore.passed = true // will trigger an error
// because passed doesnt exist on the inferred type
Explicit object type annotations
const tomScore: {name: string; score: number; } = {
name: "Tom",
score: 70
}
type Score = {name: string; score: number;}
const tomScore: Score = { name: "Tom", score: 70 }
const bobScore: Score = { name: "Bob", score: 80 }
// onClick is optional
interface ButtonProps {
text: string;
onClick?: () => void;
}
const BuyButton: ButtonProps = {
text: "Buy",
onClick: () => console.log("Buy")
}
Make things read-only
interface ButtonProps {
readonly text: string;
}
const buyButton: ButtonProps = {
text: "Buy"
}
BuyButton.text = "Sell" // ERROR
interface Result {
readonly name: string;
readonly score: number[];
}
let billScores: Result = {
name: "Bill",
scores: [90, 65, 80
}
// Important! This will NOT trigger an ERROR
billScores.scores.push(70)
interface ImmutableResult {
readonly name: string;
readonly scores: readonly number[];
}
let tomScores: ImmutableResult = {
name: "Tom",
scores: [50, 95, 80]
}
tomScores.scores.push(70) // Will trigger an ERROR!
Extending interfaces
interface ColoredButtonProps extends ButtonProps {
color: string;
}
Interfaces vs type aliases types and interfaces are similar and it is generally personal preferences as to which approach to use when creating types. before there where more differences, but now they're very similar.
let age: number | null |undefined
let possibleFruits: "banana" | "apple" | "pear"
// might be useful for react reducers
type Actions = { type: "loading" } | { type: "loaded", data: { name: string } }
type Name = {
firstName: string;
lastName: string;
}
type PhoneNumber = {
landline: string;
mobile: string;
}
type Contact = Name & PhoneNumber
const fred: Contact {
firstName: "Fred",
lastName: "Smith",
landline: "9425556"
mobile: "091780513337"
}
type Email = { emailAddress: string }
type ExtendedContact = Name & PhoneNumber & Email
The code below will error
// IMPORTANT: This will create an error!
type BaseElement = {
name: string
kind: "text" | "number" | "email"
}
type TextInput = { kind: "text" }
type Field = BaseElement & TextInput
const age: Field = {
name: "Age",
kind: "number"
}
// Type '"number"' is not assignable to type '"text"'.
// when you intersect BaseElement & TextInput
// BaseElement has kind: "text" | "number" | "email"
// TextInput has kind: "text"
// THE COMMON MEMBER OF AN INTERSECTION TYPE
// IS MATHEMATICALLY INTERSECTED
Another example
type A = {
doIt: (a: string) => void
}
type B = {
doIt: (a: string, b: string) => void
}
type A_and_B = A & B
// can also be `B & A` it doesn't matter
// this will NOT error
const ab1: A_and_B = {
doIt: (a: string ) => {}
}
// this will error
const ab2: A_and_B = {
doIt: (a:string, b: string) => {}
}
interfaces can be extended, but type aliases can also extend existing types using the intersection operator
let firstName: string = "fred"
let age = 30
firstName = age
// ERROR: Type 'number' is not assignable to type 'string'.
const jones: "Tom" | "Bob" = "Tom"
let jane: string = "jane"
jane = jones // no error
Type names are not important It's the structure that is important
type Person = { name: string }
interface IPerson { name: string }
const bob: Person = { name: "bob" }
const fred: IPerson = { name: "fred" }
let person = bob;
person = fred // Okay
let bob = { name: "bob" }
let oldFred = { name: "fred", age: 50 }
let person = bob
person = oldFred
// no error, even if oldFred has age, and person does not have that property
But this will error
let bob = { name: "bob" }
let oldFred = { name: "fred", age: 50 }
let person = bob
person = oldFred
oldFred = bob // ERROR
// Property 'age' is missing in type '{ name: string; }' but required in type '{ name: string; age: number; }'.
Typescript uses structural typing which means variables with different types can be assigned to one another if the types are compatible
- var
a
can be assigned to varb
ifb
type is wider thana
type - object
a
can be assigned to objectb
ofa
has at least the same members asb
, (a
can have properties thanb
don't havea
can still be assigned tob
) - function
a
can be assigned tob
if each parameter ina
has a corresponding parameter inb
with a compatible type
let multiply = (a: number, b: number, c: number): number => a * b * c
let subtract = (x: number, y: number ) => x - y
// subtract = multiply
// Type '(a: number, b: number, c: number) => number'
// is not assignable to type '(x: number, y: number) => number'.
multiply = subtract // no error (`subtract` has less parameters than `multiply`)
IMPORTANT! Because a function with two parameters fits into a function with three parameters. If function parameters are a subset of the parameters of another function, it can be assigned to it WHY??