Skip to content

Instantly share code, notes, and snippets.

@offirgolan
Last active September 19, 2025 17:24
Show Gist options
  • Select an option

  • Save offirgolan/51134b82f526aafd9a9dd9d112e3cc14 to your computer and use it in GitHub Desktop.

Select an option

Save offirgolan/51134b82f526aafd9a9dd9d112e3cc14 to your computer and use it in GitHub Desktop.
Extract ICU Message Argument Types
/**
* Utility type to replace a string with another.
*/
type Replace<S extends string, R extends string, W extends string> =
S extends `${infer BS}${R}${infer AS}`
? Replace<`${BS}${W}${AS}`, R, W>
: S
/**
* Utility type to remove all spaces and new lines from the provided string.
*/
type StripWhitespace<S extends string> = Replace<Replace<S, '\n', ''>, ' ', ''>;
/**
* Utility type to remove escaped characters.
*
* @example "'{word}" -> "word}"
* @example "foo '{word1} {word2}'" -> "foo "
*/
type StripEscaped<S extends string> =
S extends `${infer A}'${string}'${infer B}` ? StripEscaped<`${A}${B}`> :
S extends `${infer A}'${string}${infer B}` ? StripEscaped<`${A}${B}`> :
S;
/**
* Extract ICU message arguments from the given string.
*/
type ExtractArguments<S extends string> =
/* Handle {arg0,selectordinal,...}} since it has nested {} */
S extends `${infer A}{${infer B}}}${infer C}`
? ExtractArguments<A> | _ExtractComplexArguments<B> | ExtractArguments<C> :
/* Handle remaining arguments {arg0}, {arg0, number}, {arg0, date, short}, etc. */
S extends `${infer A}{${infer B}}${infer C}`
? ExtractArguments<A> | B | ExtractArguments<C> :
never;
/**
* Handle complex type argument extraction (i.e plural, select, and selectordinal) which
* can have nested arguments.
*/
type _ExtractComplexArguments<S extends string> =
/* Handle arg0,plural,... */
S extends `${infer A},plural,${infer B}`
? ExtractArguments<`{${A},plural}`> | _ExtractNestedArguments<`${B}}`> :
/* Handle arg0,select,... */
S extends `${infer A},select,${infer B}`
? ExtractArguments<`{${A},select}`> | _ExtractNestedArguments<`${B}}`> :
/* Handle arg0,selectordinal,... */
S extends `${infer A},selectordinal,${infer B}`
? ExtractArguments<`{${A},selectordinal}`> | _ExtractNestedArguments<`${B}}`> :
never
/**
* Extract nested arguments from complex types such as plural, select, and selectordinal.
*/
type _ExtractNestedArguments<S extends string> = S extends `${infer A}{${infer B}}${infer C}`
? _ExtractNestedArguments<A> | ExtractArguments<`${B}}`> | _ExtractNestedArguments<C> :
never;
/**
* Normalize extract arguments to either `name` or `name,type`.
*/
type NormalizeArguments<TArg extends string> =
/* Handle "name,type,other args" */
TArg extends `${infer Name},${infer Type},${string}` ? `${Name},${Type}` :
/* Handle "name,type" */
TArg extends `${infer Name},${infer Type}` ? `${Name},${Type}` :
/* Handle "name" */
TArg;
/**
* Convert ICU type to TS type.
*/
type Value<T extends string> =
T extends 'number' | 'plural' | 'selectordinal' ? number :
T extends 'date' | 'time' ? Date :
string;
/**
* Create an object mapping the extracted key to its type.
*/
type ArgumentsMap<S extends string> = {
[key in S extends `${infer Key},${string}` ? Key : S]: Extract<S, `${key},${string}`> extends `${string},${infer V}` ? Value<V>: string;
}
/**
* Create an object mapping all ICU message arguments to their types.
*/
type MessageArguments<T extends string> = ArgumentsMap<NormalizeArguments<ExtractArguments<StripEscaped<StripWhitespace<T>>>>>;
/* ======================= */
const message1 = '{name00} Foo bar {name0} baz {name1, number} bars {name2, number, ::currency} foos{name3, date, short}'
const message2 = `{name00} Foo bar {name0} baz {name1, number} bars {name2, number, ::currency} You have {numPhotos, plural,
=0 {no photos {nested, date, short}.}
=1 {one photo.}
other {# photos.}
}. {gender, select,
male {He {nested1, number}}
female {She}
other {They}
} will respond shortly. It's my cat's {year, selectordinal,
one {#st {nested2}}
two {#nd}
few {#rd}
other {#th}
} birthday!`
const message3 = "Message without arguments";
const message4 = "{count, plural, =0 {} =1 {We accept {foo}.} other {We accept {bar} and {foo}.}}";
const message5 = `{gender, select,
male {He {nested1, number}}
female {She}
other {They}
} will respond shortly.`
const message6 = `It's my cat's {year, selectordinal,
one {#st {nested2}}
two {#nd}
few {#rd}
other {#th}
} birthday!`
const message7 = `{name00} Foo bar {name0} baz {name1, number} This '{isn''t}' obvious. '{name2, number, ::currency}' foos'{name3, date, short}`
const message8 = `Our price is <boldThis>{price, number, ::currency/USD precision-integer}</boldThis>
with <link>{pct, number, ::percent} discount</link>`
type Arguments1 = MessageArguments<typeof message1>;
type Arguments2 = MessageArguments<typeof message2>;
type Arguments3 = MessageArguments<typeof message3>;
type Arguments4 = MessageArguments<typeof message4>;
type Arguments5 = MessageArguments<typeof message5>;
type Arguments6 = MessageArguments<typeof message6>;
type Arguments7 = MessageArguments<typeof message7>;
type Arguments8 = MessageArguments<typeof message8>;
@offirgolan
Copy link
Author

👋 Hi there, its taken me a while to come back to this but I ended up completely overhauling the original implementation and created a types-only NPM package (icu-message-types).

import type { ICUMessageArguments, ICUMessageTags } from 'icu-message-types';

// Extract argument types
type Args0 = ICUMessageArguments<'Hello, {firstName} {lastName}!'>;
// Result: { firstName: string | number | boolean; lastName: string | number | boolean }

type Args1 = ICUMessageArguments<`{theme, select,
  light {The interface will be bright}
  dark {The interface will be dark}
  other {The interface will use default colors}
}`>;
// Result: { theme: 'light' | 'dark' | ({} & string) | ({} & number) | boolean | null }

// Extract tag names
type Tags = ICUMessageTags<'Click <link>here</link> to continue'>;
// Result: 'link'

Message Arguments

Format TypeScript Type Example
string string | number | boolean | null {name}
number number | `${number}` | null {count, number, ...}
date Date | number | `${number}` | null {date, date, short}
time Date | number | `${number}` | null {time, time, medium}
plural number | `${number}` | null {count, plural, one {...} other {...}}
selectordinal number | `${number}` | null {position, selectordinal, one {#st} other {#th}}
select union | string | number | boolean | null {theme, select, light {...} dark {...} other {...}}

Additional Features

  • Enhanced Value Types: Non-formatted arguments accept string | number | boolean | null for more flexible usage
  • String Number Support: Numeric formats accept both number and template literal `${number}` types
  • Comprehensive Select Matching: Select arguments with other clauses support string, number, boolean, and null
  • Literal Type Transformation: Select keys are intelligently transformed (e.g., '123' becomes '123' | 123, 'true' becomes 'true' | true)
  • Escaped content: Properly handles quoted/escaped text that shouldn't be parsed as arguments
  • Nested messages: Supports complex nested structures
  • Whitespace handling: Automatically strips whitespace, new lines, and tabs for improved parsing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment