Let's have some fun with the

type system

Let's have some fun with the

type system

Marco Schumacher

Senior Software Engineer

schummar

marco@schumacher.dev

Augsburg, Munich

Pentland Firth

pentlandfirth.com

Never heard of TypeScript?

The type system

Let's get started

Motivation

Let's have some fun with the

type system

Never heard of TypeScript?

Motivation: Translate my app

export default {
  welcomeMessage: 'Hi, {name}',
  currentTime: 'It is now {time, time, short}'
}
<div>
  <FormattedMessage
    id="myMessage"
    defaultMessage="Today is {ts, date, ::yyyyMMdd}"
    values={{ts: Date.now()}}
  />
</div>
<div>
  <Trans i18nKey="userMessagesUnread" count={count}>
  	Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message.
  </Trans>
</div>
<div>
  {t('welcomeMessage', { name: 'Marco' })}
</div>

Motivation: Translate my app

export default {
  welcomeMessage: 'Hi, {name}',
  currentTime: 'It is now {time, time, short}'
}
<div>
  {t('weclomeMesage', { user: 'Marco' })}
</div>

Wouldn't it be nice...

export default {
  welcomeMessage: 'Hi, {name}',
  currentTime: 'It is now {time, time, short}'
}

My goal: Create a translation library

  • Code completion on ids and values
  • Support ICU formatting
  • Simple, lean
  • No preprocessing, no generated files
  • Pure TypeScript

And now for something completely different

const userName: string = 'someone';
const age: number = 32;
const isBanned: boolean = false;

Primitives

Arrays, Tuples

const array: string[] = ['a', 'b', 'c'];
const tuple: [string, number, boolean] = ['a', 1, true];

Functions

function add(x: number, y: number): number {
  return x + y;
}

const referenceToAdd: (x: number, y: number) => number = add;

Basic types

Classes

class Account {
  userName: string,
  password: string,
}

const account: Account = new Account();

Objects

type User = {
  firstName: string,
  lastName: string,
}
const enum Direction {
  Up,
  Down,
  Left,
  Right,
}

const dir: Direction = Direction.Down;

Enums

Unions

Advanced types

type ClickEvent = { kind: 'click', pos: Position };
type KeyDownEvent = { kind: 'keydown', key: string };
let event: ClickEvent | KeyDownEvent;

if (event.kind === 'click') {
  console.log(event.pos);
} else {
  console.log(event.key);
}

Intersections

type Animal = {
  name: string
}

type Bear = Animal & { 
  honey: boolean 
}

const bear: Bear = getBear();
console.log(bear.name, bear.honey);

Literal types

let message: 'hello';
message = 'hello'; // OK
message = 'world'; // ERROR
type Audience = 'world' | 'Web&Wine';
type Message = `Hello ${Audience}!`

const msg1: Message = 'Hello world!'; // Ok
const msg1: Message = 'Hello Web&Wine!'; // Ok
const msg1: Message = 'Hello Augsburg'; // Error

Template Literal Types

function doSomethingWithUser(user: User) {}

const user = new NotAUser();
doSomethingWithUser(user); // Error
doSomethingWithUser(user as any); // Ok

A word about any

type Collection<T extends { id: string }> = {
  items: T[],
}

Type manipulation

Generic Types

type User = {
  name: string,
  birthday: Date,
}

type Keys = keyof User;
// type Keys = 'name' | 'birthday'

keyof operator

const user = {
  name: 'Marco',
  birthday: new Date('1988-10-16'),
}

type User = typeof User;
// type User = { name: string, birthday: Date }

typeof operator

type User = {
  name: string,
  birthday: Date,
}

type V1 = User['name' | 'birthday'];
// type V1 = string | Date

type V2 = User[keyof User];
// type V2 = string | Date

Indexed access

type Example1 = Dog extends Animal ? number : string;

Conditional Types

type WithId<T> = T extends { id: any } ? T : T & { id: string };

type T1 = WithId<{ id:number, name: string }>;
// type T1 = { id: number, name: string }

type T2 = WithId<{ name: string }>;
// type T2 = { id: string, name: string }
type UnwrapPromise<T> = T extends Promise<infer S> ? UnwrapPromise<S> : T;

type T1 = UnwrapPromise<string>;
// type T1 = string

type T2 = UnwrapPromise<Promise<string>>;
// type T2 = string

type T3 = UnwrapPromise<Promise<Promise<string>>>;
// type T3 = string

Conditional Types with inference

type Partial<T> = {
  [K in keyof T]?: T[K]
}

type Book = { title: string, description: string, length: number };
type BookUpdate = Partial<Book>;
// type BookUpdate = { title?: string, description?: string, length?: number }

Mapped Types

type StringValues<T> = {
    [K in keyof T as T[K] extends string ? K : never]: T[K]
};

type Book = { title: string, description: string, length: number };
type BookStringValues = StringValues<Book>;
// type BookStringValues = { title: string, description: string }

Fun fact: The TypeScript type system is turing complete

Proof: https://gist.github.com/hediet/63f4844acf5ac330804801084f87a6d4

So, how do we apply this?

const dict = {
  key1: 'value1',
  nested: {
    key2: 'value2',
    key3: 'value3 {param1, plural, one {one} other {many}}'
  }
} as const;

type Dict = typeof dict;
type FlatKeys<Dict> = 'key1'
  | 'nested.key2'
  | 'nested.key3';
type GetICUArgs<'value3 {param, plural, one {one} other {many}}'> = {
  param: number
}

type GetICUArgs<'value3 {param1, plural, one {one {param2, date}} other {many}}'> = {
  param: number
  param2: Date
}
function t<Key extends FlatKeys<Dict>>(key: Key, values: GetICUArgs<DeepValue<Dict, Key>>) {}
type DeepValue<Dict, 'nested.key3'> = 'value3 {param1, plural, one {one} other {many}}'

Warmup: Trim

type Whitespace = ' ' | '\t' | '\n' | '\r';

/** Remove leading and tailing whitespace */
type Trim<T> = 






type Whitespace = ' ' | '\t' | '\n' | '\r';

/** Remove leading and tailing whitespace */
type Trim<T> = T extends `${Whitespace}${infer Rest}`
  ? Trim<Rest>
  : T extends `${infer Rest}${Whitespace}`
  ? Trim<Rest>
  : T extends string
  ? T
  : never;

Task 1: FlatKeys

export type FlatKeys<T extends Record<string, unknown>> = 
  
  
  
  
export type FlatKeys<T extends Record<string, unknown>> = string &
  keyof {
    [K in keyof T as T[K] extends Record<string, unknown> ? `${string & K}.${FlatKeys<T[K]>}` : K]: 1;
  };

key1: '...'

nested: {

  key2: '...',

  key3: '...'

}

key1

`nested.${                        }`

key2

key3

'...' extends Record<string, unknown> => No

'...' extends Record<string, unknown> => No

'...' extends Record<string, unknown> => No

{ key2: '...', key3: '...' } extends Record<string, unknown> => Yes

'key2' | 'key3'

Task 2: DeepValue

export type DeepValue<T extends Record<string, unknown>, K extends string> = 
  
  
  
  
  
  
export type DeepValue<T extends Record<string, unknown>, K extends string> =
  K extends `${infer Head}.${infer Rest}`
  ? T[Head] extends Record<string, unknown>
    ? DeepValue<T[Head], Rest>
    : never
  : T[K];

Task 3.1: FindBlocks

Some text { param }

Right

type FindBlocks<Text> =
  Text extends `${string}{${infer Right}` //find first {
  ? ReadBlock<'', Right, ''> extends [infer Block, infer Tail]
    ? [Block, ...FindBlocks<Tail>] // read block and find next block for tail
    : never
  : []; // no {, return empty result


type ReadBlock<Block extends string, Tail extends string, Depth extends string> =
  Tail extends `${infer L1}}${infer R1}` // find first }
    ? L1 extends `${infer L2}{${infer R2}` // if preceeded by {, this opens a nested block
      ? ReadBlock<`${Block}${L2}{`, `${R2}}${R1}`, `${Depth}+`> // then continue search right of this {
      : Depth extends `+${infer Rest}` // else if depth > 0
        ? ReadBlock<`${Block}${L1}}`, R1, Rest> // then finished nested block, continue search right of first }
        : [`${Block}${L1}`, R1] // else return full block and search for next
    : []; // no }, return emptry result

L1

R1

Result: ['param']

-

-

-

Task 3.1: FindBlocks

Some text { param, plural, one {1st} }

Right

type FindBlocks<Text> =
  Text extends `${string}{${infer Right}` //find first {
  ? ReadBlock<'', Right, ''> extends [infer Block, infer Tail]
    ? [Block, ...FindBlocks<Tail>] // read block and find next block for tail
    : never
  : []; // no {, return empty result


type ReadBlock<Block extends string, Tail extends string, Depth extends string> =
  Tail extends `${infer L1}}${infer R1}` // find first }
    ? L1 extends `${infer L2}{${infer R2}` // if preceeded by {, this opens a nested block
      ? ReadBlock<`${Block}${L2}{`, `${R2}}${R1}`, `${Depth}+`> // then continue search right of this {
      : Depth extends `+${infer Rest}` // else if depth > 0
        ? ReadBlock<`${Block}${L1}}`, R1, Rest> // then finished nested block, continue search right of first }
        : [`${Block}${L1}`, R1] // else return full block and search for next
    : []; // no }, return emptry result

L1

R1

L2

R2

=> ReadBlock with

Block = 'param, plural, one {'          Tail = '1st}}'          Depth = '+'

Task 3.1: FindBlocks

type FindBlocks<Text> =
  Text extends `${string}{${infer Right}` //find first {
  ? ReadBlock<'', Right, ''> extends [infer Block, infer Tail]
    ? [Block, ...FindBlocks<Tail>] // read block and find next block for tail
    : never
  : []; // no {, return empty result


type ReadBlock<Block extends string, Tail extends string, Depth extends string> =
  Tail extends `${infer L1}}${infer R1}` // find first }
    ? L1 extends `${infer L2}{${infer R2}` // if preceeded by {, this opens a nested block
      ? ReadBlock<`${Block}${L2}{`, `${R2}}${R1}`, `${Depth}+`> // then continue search right of this {
      : Depth extends `+${infer Rest}` // else if depth > 0
        ? ReadBlock<`${Block}${L1}}`, R1, Rest> // then finished nested block, continue search right of first }
        : [`${Block}${L1}`, R1] // else return full block and search for next
    : []; // no }, return emptry result

Block = 'param, plural, one {'          Tail = '1st}}'          Depth = '+'

L1

R1

=> ReadBlock with

Block = 'param, plural, one {1st}'          Tail = '}'          Depth = ''

Text

Text

Task 3.1: FindBlocks

type FindBlocks<Text> =
  Text extends `${string}{${infer Right}` //find first {
  ? ReadBlock<'', Right, ''> extends [infer Block, infer Tail]
    ? [Block, ...FindBlocks<Tail>] // read block and find next block for tail
    : never
  : []; // no {, return empty result


type ReadBlock<Block extends string, Tail extends string, Depth extends string> =
  Tail extends `${infer L1}}${infer R1}` // find first }
    ? L1 extends `${infer L2}{${infer R2}` // if preceeded by {, this opens a nested block
      ? ReadBlock<`${Block}${L2}{`, `${R2}}${R1}`, `${Depth}+`> // then continue search right of this {
      : Depth extends `+${infer Rest}` // else if depth > 0
        ? ReadBlock<`${Block}${L1}}`, R1, Rest> // then finished nested block, continue search right of first }
        : [`${Block}${L1}`, R1] // else return full block and search for next
    : []; // no }, return emptry result

Block = 'param, plural, one {1st}'          Tail = '}'          Depth = ''

L1

R1

Result: ['param, plural, one {1st}']

Text

Text

Text

Task 3.2: ParseBlock

type ParseBlock<Block> = Block extends `${infer Name},${infer Format},${infer Rest}`
  ? { [K in Trim<Name>]: VariableType<Trim<Format>> } & TupleParseBlock<TupleFindBlocks<FindBlocks<Rest>>>
  : Block extends `${infer Name},${infer Format}`
  ? { [K in Trim<Name>]: VariableType<Trim<Format>> }
  : { [K in Trim<Block>]: Value };
type VariableType<T extends string> = T extends 'number' | 'plural' | 'selectordinal' ? number
  : T extends 'date' | 'time' ? Date
  : Value;
export type GetICUArgs<T> = T extends string ? TupleParseBlock<FindBlocks<T>> : never;

'param' => { param: Value }

'param, plural, one { 1st {nested, Date} }' => {}

'param, plural, one { 1st {nested, Date} }' => { param: }

'param, plural, one { 1st {nested, Date} }' => { param: number }

'param, plural, one { 1st {nested, Date} }' => { param: number } & {}

'param, plural, one { 1st {nested, Date} }' => { param: number } & { nested: }

'param, plural, one { 1st {nested, Date} }' => { param: number } & { nested: Date }

DEMO

schummar/schummar-translate

npm schummar-translate

Made with Slides.com