Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keep inferred type while restricting variable type

Tags:

typescript

foo variable should keep { a: { b: string } } inferred type while it's restricted to conform another type, Foo. It preferably should have type { a: { b: string } } & Foo:

type Foo = { [k: string]: { [k: string]: string } };

const _foo = { a: { b: 'c' } };
export const foo: typeof _foo & Foo = _foo;

But it's acceptable for it to have { a: { b: string } } type - as long as it produces type error in case it doesn't conform to Foo:

type Foo = { [k: string]: { [k: string]: string } };

function checkFoo(foo: Foo) {};

let foo = { a: { b: 'c' } };
checkFoo(foo);

The objective is to make TypeScript emit only single JavaScript line:

var foo = { a: { b: 'c' } }; 

checkFoo function can also present in compiler output, as long as it's not called, so it could be removed by a minifier as dead code.

I'd prefer to avoid unnecessary compiler output like checkFoo(foo) if possible.

What are the options here? Are there ones that are specific to recent TypeScript versions, 2.7 and 2.8?

like image 933
Estus Flask Avatar asked Apr 27 '18 16:04

Estus Flask


1 Answers

While I don't necessarily agree with the premise of avoiding the extra function call at all cost, we can generate a compiler error that emits no JS code if foo does not implement the expected interface without using functions.

The approach is based on creating a conditional type (available in 2.8) that returns different string literal types based on whether the type of the constant implements the interface or not. Then we can use this type in a place where we expect a specific type literal.

To get an error, we will need to use the type in a way that generates no code, and I found 2 possible ways, either in a declaration, or in a type alias.

export type Foo = { [k: string]: { [k: string]: string } };

type TestType<T, TBase> = T extends TBase ? "OK" : "Implementation does not extend expected type";
type Validate<T extends "OK"> = "OK";

let foo = { a: { b: 'c' } };
// use type in a constant 
declare const fooTest : Validate<TestType<typeof foo, Foo>> // ok
// use type in a type alias
type fooTest = Validate<TestType<typeof foo, Foo>> //ok

let fooBad = { a: { b: 10 } };
declare const fooBadTest : Validate<TestType<typeof fooBad, Foo>>;  // error: Type '"Implementation does not extend expected type"' does not satisfy the constraint '"OK"'

type fooBadTest = Validate<TestType<typeof fooBad, Foo>> // error: Type '"Implementation does not extend expected type"' does not satisfy the constraint '"OK"'.

One problem with this approach is that we have to introduce extra type aliases/ constant names that pollute the namespace.

Playground link.

like image 199
Titian Cernicova-Dragomir Avatar answered Oct 10 '22 02:10

Titian Cernicova-Dragomir